Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const processFromQueue = (n) => {
1. For the logs where `log.kind` is missing, first 40 chars of dynamic_data is populated to uniquely identify each log in a restricted fashion.
2. For the logs where no contexts are set, ie. server, process or worker are not set, server format category and version is populated.
3. `request.request_id` and the `request_start_time` can be set manually if required.
4. Credential-bearing request headers (`authorization`, `cookie`, `set-cookie`, `x-api-key`, `x-auth-token`, `proxy-authorization`, `www-authenticate`) are logged as `[REDACTED]`. The header name is kept so the log still shows it was sent. Add your own with `Chitragupta.extendSensitiveHeaders(['x-tenant-secret'])`.

## Contributing

Expand Down
1 change: 1 addition & 0 deletions lib/chitragupta/chitragupta.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,5 @@ module.exports = {
getUniqueLogId,
jsonLogFormatter,
setMetaData,
extendSensitiveHeaders: util.extendSensitiveHeaders,
};
44 changes: 42 additions & 2 deletions lib/chitragupta/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,45 @@ const formatVersions = require('./format_versions');
const fieldLimits = require('./field_limits');
const os = require('os');

// Sensitive headers that must never reach the log sink as cleartext values.
// Closes DATA-12974 (Authorization/Cookie/etc serialised verbatim) and weakens
// DATA-13003/13004 chains by removing the secret material at the source.
// Names are matched case-insensitively against the request header keys.
const DEFAULT_SENSITIVE_HEADERS = new Set([
'authorization',
'cookie',
'set-cookie',
'x-api-key',
'x-auth-token',
'proxy-authorization',
'www-authenticate',
]);

const customSensitiveHeaders = new Set();

function extendSensitiveHeaders(headerNames) {
if (!Array.isArray(headerNames)) return;
headerNames.forEach((h) => {
if (typeof h === 'string' && h.length) {
customSensitiveHeaders.add(h.toLowerCase());
}
});
}

function isSensitiveHeader(name) {
const lower = String(name).toLowerCase();
return DEFAULT_SENSITIVE_HEADERS.has(lower) || customSensitiveHeaders.has(lower);
}

function redactHeaders(headers) {
if (!headers || typeof headers !== 'object') return headers;
const out = {};
Object.keys(headers).forEach((key) => {
out[key] = isSensitiveHeader(key) ? '[REDACTED]' : headers[key];
});
return out;
}

function populateServerData(dataParam) {
const data = dataParam;
const request = cls.get('request');
Expand All @@ -17,7 +56,7 @@ function populateServerData(dataParam) {
const indexOfQuestionMark = url.indexOf('?');
let endpoint = '';
let params = '';
let headers = JSON.stringify(request.headers);
let headers = JSON.stringify(redactHeaders(request.headers));
if (indexOfQuestionMark > 0) {
endpoint = url.slice(0, indexOfQuestionMark);
params = url.slice(indexOfQuestionMark + 1);
Expand All @@ -40,7 +79,7 @@ function populateServerData(dataParam) {
data.data.request.params = JSON.stringify(data.data.request.params);
}
if (data.data.request.headers && typeof data.data.request.headers === 'object') {
data.data.request.headers = JSON.stringify(data.data.request.headers);
data.data.request.headers = JSON.stringify(redactHeaders(data.data.request.headers));
}
data.data.request.params = data.data.request.params.substring(0, fieldLimits.PARAMS) || '';

Expand Down Expand Up @@ -135,3 +174,4 @@ function sanitizeKeys(logLevel, message, metaData) {
}

module.exports.sanitizeKeys = sanitizeKeys;
module.exports.extendSensitiveHeaders = extendSensitiveHeaders;
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "chitragupta",
"version": "1.7.6",
"version": "1.7.7",
"description": "An easy to install node module to convert unstructured logs into informative structured logs",
"main": "lib/index.js",
"scripts": {
"test": "node test/context.js"
"test": "node test/context.js && node test/headers.js"
},
"repository": {
"type": "git",
Expand Down
65 changes: 65 additions & 0 deletions test/headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const assert = require('assert');
const EventEmitter = require('events');
const { Chitragupta } = require('../lib');

function response() {
const res = new EventEmitter();
res.finished = true;
res.statusCode = 200;
res.duration = 1;
return res;
}

function formatted(headers) {
let record;
Chitragupta.setupServerLogger(
{ log() {} },
{ url: '/api/resource?page=2', method: 'GET', headers },
response(),
'user-1',
() => {
record = JSON.parse(Chitragupta.jsonLogFormatter({
level: 'info',
message: 'Done',
meta: { log: { kind: 'HEADER_TEST' } },
}));
},
);
return JSON.parse(record.data.request.headers);
}

function testSensitiveHeadersAreRedacted() {
const logged = formatted({
host: 'app.example.com',
authorization: 'Bearer secret-token',
cookie: 'session=secret-session',
'x-api-key': 'secret-key',
});

assert.strictEqual(logged.authorization, '[REDACTED]');
assert.strictEqual(logged.cookie, '[REDACTED]');
assert.strictEqual(logged['x-api-key'], '[REDACTED]');
assert.strictEqual(logged.host, 'app.example.com');
}

function testMatchIsCaseInsensitive() {
assert.strictEqual(formatted({ Authorization: 'Bearer secret-token' }).Authorization, '[REDACTED]');
}

function testRequestHeadersAreNotMutated() {
const headers = { authorization: 'Bearer secret-token' };
formatted(headers);
assert.strictEqual(headers.authorization, 'Bearer secret-token');
}

function testHostsCanExtendTheDenylist() {
assert.strictEqual(formatted({ 'x-tenant-secret': 'secret' })['x-tenant-secret'], 'secret');
Chitragupta.extendSensitiveHeaders(['X-Tenant-Secret']);
assert.strictEqual(formatted({ 'x-tenant-secret': 'secret' })['x-tenant-secret'], '[REDACTED]');
}

testSensitiveHeadersAreRedacted();
testMatchIsCaseInsensitive();
testRequestHeadersAreNotMutated();
testHostsCanExtendTheDenylist();
process.stdout.write('header redaction tests passed\n');
Loading