Files
multi-simadmin/packages/test-fixtures/test/security-regressions.test.ts
T
chick f11877f13e feat(contracts): share notification, organization and device capability models
Move the Hub channel table, organization tags and device capability definitions
into the workspace packages so the control plane and the console validate the
same contract, and refresh the frozen upstream evidence for the new paths.
2026-09-05 18:52:54 +08:00

312 lines
9.3 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { upstream58e2204Operations } from '../../operation-registry/src/upstream-58e2204.ts';
import {
collectOne,
selectReadonlyOperations,
validateReadonlyOperation,
validateInstanceOrigin,
} from '../scripts/collector.ts';
import { loadInstances } from '../scripts/collect-readonly-fixtures.ts';
import { redact, redactOperationPayload, parseResponseSensitivePath } from '../src/redactor.ts';
const health = upstream58e2204Operations.find((x: any) => x.operationId === 'getHealth')!;
const fakeResponse = async () =>
new Response('{"ok":true}', { headers: { 'content-type': 'application/json' } });
test('operation validator rejects path and registry identity bypasses', async () => {
const attacks = [
'https://example.invalid/api/health',
'//example.invalid/api/health',
'/api/health?scan=1',
'/api/health#x',
'/api/%73can',
'/api/%252e%252e/health',
'/api/a/../health',
'/api\\health',
'/api//health',
];
for (const pathTemplate of attacks)
assert.throws(() => validateReadonlyOperation({ ...health, pathTemplate }));
assert.throws(() => validateReadonlyOperation({ ...health }));
assert.throws(() =>
validateReadonlyOperation({
operationId: health.operationId,
method: 'GET',
riskLevel: 'R0',
pathTemplate: health.pathTemplate,
}),
);
await assert.rejects(() =>
collectOne({ origin: 'http://192.168.1.2', alias: 'instance-1' }, { ...health }, fakeResponse),
);
});
test('all path bypass classes fail before transport', async () => {
let calls = 0;
const transport = async () => {
calls++;
return new Response('{}', { headers: { 'content-type': 'application/json' } });
};
const attacks = [
'https://evil.invalid/api/health',
'//evil.invalid/api/health',
'/api/health?q=1',
'/api/health#x',
'/api/%68ealth',
'/api/%252e%252e/x',
'/api/a/../x',
'/api\\health',
'/api//health',
];
for (const pathTemplate of attacks)
await assert.rejects(() =>
collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
{ ...health, pathTemplate },
transport,
),
);
assert.equal(calls, 0);
assert.throws(() => selectReadonlyOperations([{ ...health }]));
});
test('collectOne validates RFC1918 origin before transport', () => {
for (const origin of [
'http://127.0.0.1',
'http://169.254.1.1',
'http://100.100.100.200',
'http://8.8.8.8',
'http://router.local',
'http://u:p@192.168.1.2',
'http://192.168.1.2/x',
'http://192.168.1.2?q=1',
'ftp://192.168.1.2',
])
assert.throws(() => validateInstanceOrigin(origin));
assert.equal(validateInstanceOrigin('https://172.16.2.3:8443'), 'https://172.16.2.3:8443');
});
test('passive selection explicitly denies connectivity and active scan', () => {
const { selected, denied } = selectReadonlyOperations(upstream58e2204Operations);
assert.ok(denied.some((x: any) => x.pathTemplate === '/api/connectivity'));
assert.ok(denied.some((x: any) => x.pathTemplate === '/api/network/operators/scan'));
assert.ok(!selected.some((x: any) => x.pathTemplate === '/api/connectivity'));
});
test('config uses formal validation plus collector LAN/auth/origin constraints', () => {
const good = {
instances: [
{ id: 'a', url: 'http://192.168.1.2', auth: { mode: 'none' } },
{ id: 'b', url: 'https://10.0.0.2', auth: { mode: 'none' } },
],
};
assert.deepEqual(
loadInstances(good).map((x: any) => x.alias),
['instance-1', 'instance-2'],
);
const bad = [
'http://127.0.0.1',
'http://169.254.1.2',
'http://100.100.100.200',
'http://8.8.8.8',
'http://router.local',
'http://u:***@192.168.1.2',
'http://192.168.1.2/path',
'http://192.168.1.2?q=1',
'http://192.168.1.2#x',
'http://[::1]',
];
for (const url of bad)
assert.throws(() =>
loadInstances({
instances: [
{ id: 'a', url },
{ id: 'b', url: 'http://10.0.0.2' },
],
}),
);
assert.throws(() =>
loadInstances({
instances: [
{ id: 'a', url: 'http://192.168.1.2', auth: { password: 'x' } },
{ id: 'b', url: 'http://10.0.0.2' },
],
}),
);
assert.throws(() => loadInstances({ instances: [{ id: 'a', url: 'http://192.168.1.2' }] }));
assert.throws(() =>
loadInstances({
instances: [
{ id: 'a', url: 'http://192.168.1.2' },
{ id: 'b', url: 'http://10.0.0.2' },
{ id: 'c', url: 'http://10.0.0.3' },
],
}),
);
});
test('directional response JSONPaths redact every declared path and preserve shape/types', () => {
const ops = upstream58e2204Operations.filter((op: any) =>
op.sensitiveFields.some((f: any) => f.direction === 'response'),
);
assert.ok(ops.length > 0);
for (const op of ops)
for (const field of op.sensitiveFields.filter((f: any) => f.direction === 'response')) {
const parts = parseResponseSensitivePath(field.path);
assert.ok(parts.length > 0, `${op.operationId} unsupported response path`);
let leaf: any = { s: 'private', n: 42, b: true, z: null, a: ['x', { q: 9 }] };
let payload: any = leaf;
for (let i = parts.length - 1; i >= 0; i--) {
const p = parts[i];
payload = p === '*' ? [payload] : { [p]: payload };
}
const out: any = redactOperationPayload(payload, op);
let hit = out;
for (const p of parts) hit = p === '*' ? hit[0] : hit[p];
assert.deepEqual(hit, {
s: '[REDACTED]',
n: 0,
b: false,
z: null,
a: ['[REDACTED]', { q: 0 }],
});
}
});
test('deep sanitizer handles reviewed key/value bypasses without schema destruction', () => {
const input: any = {
credential: 'x',
api_key: 'x',
access_key: 'x',
device_key: 'x',
private_key: 'x',
pin: 12,
puk: 34,
username: 'x',
user: 'x',
account: 'x',
serial: 'x',
revision: 'x',
path: '/private/a',
template: 'user=x token=abc123 path=/private/a',
config: { access_token: 'tiny' },
phone_number_is_manual: true,
sms_center_is_manual: false,
message: 'API is healthy',
nested: '{"username":"x","enabled":true}',
host: 'private.internal:8080',
short_number: '12345678',
split_id: '12-34-56-78-90-12-34',
ipv6: '::1%lo0',
latitude: 1.234567,
lon: 2.345678,
cell_id: 99,
lac: 2,
tac: 3,
pci: 4,
enb: 5,
gnb: 6,
uptime: 777,
traffic: 888,
timestamp: '2026-07-16T12:34:56Z',
unknown: 'AKIAIOSFODNN7EXAMPLE',
};
const x: any = redact(input);
for (const k of [
'credential',
'api_key',
'access_key',
'device_key',
'private_key',
'username',
'user',
'account',
'serial',
'revision',
'path',
'template',
'host',
'short_number',
'split_id',
'ipv6',
'timestamp',
'unknown',
])
assert.equal(x[k], '[REDACTED]');
for (const k of [
'pin',
'puk',
'latitude',
'lon',
'cell_id',
'lac',
'tac',
'pci',
'enb',
'gnb',
'uptime',
'traffic',
])
assert.equal(x[k], 0);
assert.equal(typeof x.nested, 'string');
assert.deepEqual(JSON.parse(x.nested), { username: '[REDACTED]', enabled: true });
assert.deepEqual(x.config, { access_token: '[REDACTED]' });
assert.equal(x.phone_number_is_manual, true);
assert.equal(x.sms_center_is_manual, false);
assert.equal(x.message, 'API is healthy');
});
test('schema shapes and primitive types survive conservative redaction', () => {
const x: any = redact({
phone_numbers: ['12345678', '87654321'],
ip_addresses: [{ v4: '192.168.1.9', active: true }],
urls: ['private.example/x'],
webhook: { url: '//private.example/h', enabled: true },
config: { s: 'secret', n: 42, b: true, z: null, a: ['x', { n: 9 }] },
});
assert.ok(Array.isArray(x.phone_numbers));
assert.equal(x.phone_numbers.length, 2);
assert.ok(Array.isArray(x.ip_addresses));
assert.equal(typeof x.ip_addresses[0], 'object');
assert.ok(Array.isArray(x.urls));
assert.equal(typeof x.webhook, 'object');
assert.deepEqual(x.config, {
s: '[REDACTED]',
n: 0,
b: false,
z: null,
a: ['[REDACTED]', { n: 0 }],
});
});
test('invalid declared JSON and non-JSON bodies are non-json and discarded', async () => {
const invalid = await collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
health,
async () =>
new Response('{bad', { status: 200, headers: { 'content-type': 'application/json' } }),
);
assert.equal(invalid.statusCategory, 'non-json');
assert.deepEqual(invalid.payload, { error: 'invalid-json' });
const text = await collectOne(
{ origin: 'http://192.168.1.2', alias: 'instance-1' },
health,
async () =>
new Response('private body', { status: 200, headers: { 'content-type': 'text/plain' } }),
);
assert.equal(text.statusCategory, 'non-json');
assert.deepEqual(text.payload, { text: '[REDACTED]' });
});
test('collector errors never echo rejected origin values', async () => {
const secret = 'http://user:password@evil.invalid/private?token=x';
try {
await collectOne({ origin: secret, alias: 'instance-1' }, health, fakeResponse);
assert.fail('expected rejection');
} catch (error: any) {
assert.ok(!String(error.message).includes(secret));
assert.ok(!String(error.message).includes('password'));
}
});