110 lines
3.9 KiB
JavaScript
110 lines
3.9 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const {
|
|
formatAltaError,
|
|
sanitizeErrorMessage,
|
|
} = require('../src/error-redaction');
|
|
|
|
const SENTINEL = 'HERMES_SENTINEL_SECRET';
|
|
|
|
function assertNoSecret(value) {
|
|
const serialized = JSON.stringify(value);
|
|
assert.equal(serialized.includes(SENTINEL), false, serialized);
|
|
assert.equal(serialized.includes('Cookie'), false, serialized);
|
|
assert.equal(serialized.includes('Authorization'), false, serialized);
|
|
}
|
|
|
|
test('formats only an allowlist of bounded safe fields', () => {
|
|
const error = new Error('request failed safely');
|
|
error.code = 'ECONNRESET';
|
|
error.timeout = false;
|
|
error.response = { status: 502 };
|
|
|
|
const formatted = formatAltaError('getDevices', error, { secrets: [SENTINEL] });
|
|
assert.deepEqual(formatted, {
|
|
operation: 'getDevices',
|
|
code: 'ECONNRESET',
|
|
status: 502,
|
|
timeout: false,
|
|
message: 'request failed safely',
|
|
});
|
|
assert.deepEqual(Object.keys(formatted), ['operation', 'code', 'status', 'timeout', 'message']);
|
|
});
|
|
|
|
test('never serializes Axios config, request, headers, URL, body, cause or response data', () => {
|
|
const error = new Error(`failed with va=${SENTINEL}`);
|
|
error.code = 'ERR_BAD_RESPONSE';
|
|
error.config = {
|
|
url: `https://evil.example/?token=${SENTINEL}`,
|
|
headers: { Cookie: `va=${SENTINEL}`, Authorization: `Bearer ${SENTINEL}` },
|
|
data: { token: SENTINEL },
|
|
};
|
|
error.request = { rawHeaders: `Cookie: va=${SENTINEL}` };
|
|
error.response = {
|
|
status: 401,
|
|
headers: { 'set-cookie': `va=${SENTINEL}` },
|
|
data: { message: SENTINEL, nested: { secret: SENTINEL } },
|
|
};
|
|
error.cause = { message: SENTINEL, headers: { Cookie: SENTINEL } };
|
|
|
|
const formatted = formatAltaError('getDevices', error, { secrets: [SENTINEL] });
|
|
assertNoSecret(formatted);
|
|
assert.deepEqual(Object.keys(formatted), ['operation', 'code', 'status', 'timeout', 'message']);
|
|
assert.equal(formatted.message, 'failed with va=[REDACTED]');
|
|
});
|
|
|
|
test('redacts cookie and bearer patterns without needing the exact secret', () => {
|
|
for (const message of [
|
|
`Cookie: va=${SENTINEL}; other=value`,
|
|
`va=${SENTINEL}`,
|
|
`Authorization: Bearer ${SENTINEL}`,
|
|
`Bearer ${SENTINEL}`,
|
|
]) {
|
|
const clean = sanitizeErrorMessage(message);
|
|
assertNoSecret({ message: clean });
|
|
}
|
|
});
|
|
|
|
test('bounds and sanitizes operation, code, status and message', () => {
|
|
const error = {
|
|
message: `line one\r\nCookie: va=${SENTINEL} ${'x'.repeat(1000)}`,
|
|
code: 'BAD CODE WITH SPACES AND SECRET_' + SENTINEL,
|
|
response: { status: 9999 },
|
|
};
|
|
const formatted = formatAltaError(`unsafe operation ${SENTINEL}`, error, { secrets: [SENTINEL] });
|
|
|
|
assertNoSecret(formatted);
|
|
assert.ok(formatted.operation.length <= 64);
|
|
assert.equal(formatted.code, 'ALTA_ERROR');
|
|
assert.equal(formatted.status, null);
|
|
assert.ok(formatted.message.length <= 240);
|
|
assert.equal(/[\r\n]/.test(formatted.message), false);
|
|
});
|
|
|
|
test('handles hostile accessors, cycles and non-error values without leaking or throwing', () => {
|
|
const hostile = {};
|
|
Object.defineProperty(hostile, 'message', { get() { throw new Error(SENTINEL); } });
|
|
Object.defineProperty(hostile, 'code', { get() { throw new Error(SENTINEL); } });
|
|
hostile.self = hostile;
|
|
|
|
const formatted = formatAltaError('request', hostile, { secrets: [SENTINEL] });
|
|
assertNoSecret(formatted);
|
|
assert.equal(formatted.message, 'Alta request failed');
|
|
assert.equal(formatted.code, 'ALTA_ERROR');
|
|
|
|
assertNoSecret(formatAltaError('request', SENTINEL, { secrets: [SENTINEL] }));
|
|
assertNoSecret(formatAltaError('request', null, { secrets: [SENTINEL] }));
|
|
});
|
|
|
|
test('does not leak a supplied secret through valid-looking operation or code fields', () => {
|
|
const formatted = formatAltaError(SENTINEL, {
|
|
code: SENTINEL,
|
|
message: 'failed',
|
|
}, { secrets: [SENTINEL] });
|
|
assertNoSecret(formatted);
|
|
assert.equal(formatted.operation, 'altaRequest');
|
|
assert.equal(formatted.code, 'ALTA_ERROR');
|
|
});
|