Capture Request Bodies in Fastify Without Leaking Secrets
Capture Fastify request bodies for forensics with a preHandler hook that redacts passwords and tokens first, plus handling multipart and responses.
Capture Request Bodies in Fastify Without Leaking Secrets
With SecureNow 8.11.2, automatic capture also redacts code, otpCode, oneTimeCode, verificationCode, and passcode credentials in bodies, URL queries, and captured headers. Case/separator variants are covered; status_code and error_code remain useful in traces. Keep this rule in manual capture utilities too.
Fastify's preHandler hook is the place. Bodies are parsed, route is matched, and you can attach the redacted version to your trace span before the handler runs.
The hook
import { trace } from '@opentelemetry/api';
const OTP_FIELDS = new Set(['code', 'otp', 'otpcode', 'onetimecode', 'onetimepassword', 'passcode', 'verificationcode', 'confirmationcode', 'emailcode', 'smscode', 'logincode', 'signincode', 'authcode', 'authenticationcode', 'mfacode', 'totpcode', '2facode', 'securitycode', 'recoverycode', 'backupcode']);
const isOtpField = (key) => OTP_FIELDS.has(key.replace(/^x[-_]/i, '').replace(/[^a-z0-9]/gi, '').toLowerCase());
const SENSITIVE = new Set([
'password', 'pass', 'pwd', 'token', 'access_token', 'refresh_token',
'api_key', 'apikey', 'secret', 'authorization', 'cookie', 'session',
'creditcard', 'card_number', 'cvv', 'cvc', 'ssn',
]);
function redact(obj, depth = 0) {
if (depth > 10) return '[max-depth]';
if (obj == null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.map(item => redact(item, depth + 1));
const result = {};
for (const [key, value] of Object.entries(obj)) {
if (SENSITIVE.has(key.toLowerCase()) || isOtpField(key)) result[key] = '[REDACTED]';
else if (typeof value === 'object') result[key] = redact(value, depth + 1);
else result[key] = value;
}
return result;
}
export async function forensicsHook(req, reply) {
const span = trace.getActiveSpan();
if (!span) return;
if (req.body && typeof req.body === 'object') {
const safe = JSON.stringify(redact(req.body));
span.setAttribute('http.request.body', safe.slice(0, 8192));
}
span.setAttribute('http.request.headers', JSON.stringify(redactHeaders(req.headers)));
}
function redactHeaders(headers) {
const result = {};
for (const [key, value] of Object.entries(headers)) {
const lower = key.toLowerCase();
result[key] = (lower === 'authorization' || lower === 'cookie' || lower.startsWith('x-api-key') || isOtpField(key))
? '[REDACTED]'
: String(value);
}
return result;
}
Wiring globally
import Fastify from 'fastify';
import { forensicsHook } from './forensics.js';
const app = Fastify({ trustProxy: 1 });
app.addHook('preHandler', forensicsHook);
Multipart bodies
Skip file content; capture only field metadata:
async function safeMultipart(req) {
if (!req.isMultipart?.()) return null;
const fields = {};
for await (const part of req.parts()) {
if (part.type === 'field') {
fields[part.fieldname] = SENSITIVE.has(part.fieldname.toLowerCase())
? '[REDACTED]'
: part.value;
} else {
fields[part.fieldname] = `[file: ${part.filename}, ${part.mimetype}]`;
}
}
return fields;
}
Response capture
app.addHook('onSend', async (req, reply, payload) => {
const span = trace.getActiveSpan();
if (span && payload && reply.getHeader('content-type')?.includes('json')) {
try {
const parsed = JSON.parse(payload);
span.setAttribute('http.response.body', JSON.stringify(redact(parsed)).slice(0, 8192));
} catch {}
}
return payload;
});
The automatic option
node -r securenow/register server.js
Auto-captures request bodies and headers with redaction across Fastify routes (response bodies are not captured). The hand-rolled version above gives you fine control; SecureNow gives you maintenance-free.
Related
Frequently Asked Questions
Why preHandler instead of onRequest?
preHandler runs after body parsing, so `req.body` is populated. onRequest runs before parsing — no body to capture.
Does this work with all body types (JSON, multipart, etc.)?
JSON yes, multipart needs special handling because the file streams shouldn't be captured (huge). Multipart bodies should capture only the field metadata, not file contents.
Is there an automatic option?
Yes — preload `securenow/register` and the SDK auto-captures bodies for Fastify with redaction across all routes.
Recommended reading
Block one IP, a CIDR range, or a single route and method in a Node.js app within seconds, with no code change: setup, scoping, CLI, MCP, verification and undo.
Oct 27Built your app with a coding agent? 11 warning signs it needs a security audit, each checkable in five minutes, and how to run the full audit yourself.
Oct 26Part 1 of the AI security audit series: set up an AI coding agent to audit your app locally, connect SecureNow, and run your first threat-model pass.
Jul 24