Redact credentials from logged HTTP request headers (#1054)

The controller HTTP access log and the express error log wrote the full
request headers at the http level, which both default log transports
keep. Clients send their access token in X-Access-Token or
Authorization: Bearer, so user and host tokens ended up in plain text
in controller and cluster logs, and the cluster log can be read back
over the API with core.log.query.

Mask the authorization, x-access-token, cookie and proxy-authorization
headers before logging.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
BastiaanandClaude Opus 5.5 authored and GitHub committed 2026-10-08 15:45:43 +01:00
1 parent 069f537bde
commit fe1391903a
2 files changed
+36 -2

No files matched your search

+18 -2
View File
@@ -409,7 +409,7 @@ export default class Controller {
method: req.method,
url: req.originalUrl,
httpVersion: req.httpVersion,
headers: req.headers,
headers: Controller.loggableHeaders(req.headers),
query: req.query,
},
});
@@ -897,7 +897,7 @@ export default class Controller {
statusCode: res.statusCode,
responseTime: durationMs,
httpVersion: req.httpVersion,
headers: req.headers,
headers: Controller.loggableHeaders(req.headers),
query: req.query,
},
});
@@ -1226,6 +1226,22 @@ export default class Controller {
return url;
}
/**
* Copy of request headers with credentials masked for logging
*
* @param headers - headers of the incoming request.
* @returns headers safe to write to the log.
*/
static loggableHeaders(headers: http.IncomingHttpHeaders) {
const result = { ...headers };
for (const name of ["authorization", "cookie", "proxy-authorization", "x-access-token"]) {
if (result[name] !== undefined) {
result[name] = "[redacted]";
}
}
return result;
}
/**
* Serves the web interface with the root path set appropriately
*
+18
View File
@@ -86,6 +86,24 @@ describe("controller/src/Controller", function() {
await assert.rejects(Controller.loadJsonObject(path.join(dir, "invalid.json")), SyntaxError);
});
});
describe(".loggableHeaders()", function() {
it("should redact credentials and keep other headers", function() {
const headers = {
"authorization": "Bearer secret",
"x-access-token": "secret",
"cookie": "a=secret",
"user-agent": "test",
};
assert.deepEqual(Controller.loggableHeaders(headers), {
"authorization": "[redacted]",
"x-access-token": "[redacted]",
"cookie": "[redacted]",
"user-agent": "test",
});
assert.equal(headers["x-access-token"], "secret");
});
});
describe(".serveWeb()", function() {
let server, port;
before(async function() {