Last reviewed: November 4, 2026 (Asia/Shanghai); sources fetched November 3 UTC.
Direct answer
Prevent response-header injection in a coding-agent patch by keeping header names application-owned, validating each final value against its field’s intended grammar, and using the framework’s structured response API. Complete decoding, formatting, and validation before sending headers or body bytes. Reject invalid values through a fixed error path; do not recover by writing a raw HTTP response or removing a few suspicious characters.
A typical change copies a query parameter, stored label, or upstream value into an outgoing header. If carriage return (CR) or line feed (LF) reaches a permissive serializer, data can become a new header or message boundary. MITRE CWE-113 describes this weakness and possible consequences including response splitting and cache poisoning. A maintained runtime may already reject these characters: Node.js’s HTTP API documents automatic header validation and exceptions for invalid names or values. Preserve that protection and review what the application authorizes around it.
The acceptance criteria below address application-generated outgoing headers. For a redirect, also apply the site’s open-redirect review
: a syntactically valid Location can still select an unauthorized destination. For disagreement about HTTP message lengths across intermediaries, use the separate request-smuggling review
.
Who this is for
Use this guide when an agent changes response builders, redirect helpers, download metadata, cookie construction, custom diagnostic fields, proxy response mapping, or error middleware. Search the diff for setHeader, writeHead, header-map spreads, custom status messages, and direct socket writes. Trace values from requests, database records, imported files, model output, and upstream services through every transformation to the response API.
The running example is deliberately narrow: a route may echo one request tag in the fixed field X-Request-Tag. Its tag contains 1–64 ASCII letters, digits, underscores, or hyphens. This is an example application policy, not a general HTTP restriction or a universal sanitizer.
Key takeaways
- Keep the set of outgoing fields under application control. A legal header name does not authorize a caller to choose it.
- Validate the representation that will actually be serialized, after the route’s documented decoding and formatting steps.
- Preserve automatic runtime validation. Explicit preflight checks can move a failure earlier, before the response starts.
- Check field meaning as well as syntax. Redirect targets, cookies, filenames, cache policy, and framing have different contracts.
- Reject invalid input without reflecting it in headers, bodies, status messages, or logs.
- Test the HTTP response and any proxy transformations, including errors and late middleware overrides.
Contract details to verify
Define the field and its authority
Write one contract for every dynamic outgoing field. The tag example needs a fixed name, a string-only input, an exact character set, a 64-character ceiling, one decoding owner, and a controlled rejection before response commitment. Because its accepted alphabet is ASCII, the character and byte ceilings coincide.
| Decision | Tag-route example |
|---|---|
| Who chooses the field name? | Application code; always X-Request-Tag |
| Who supplies the value? | Untrusted decoded query value |
| What does it mean? | Inert request label; no routing or authorization role |
| Which values are accepted? | 1–64 ASCII letters, digits, underscores, or hyphens |
| When is validation complete? | Before response headers or body are sent |
| What happens on rejection? | Fixed 400 body, no echoed tag, no dependent action |
| What reaches diagnostics? | Fixture ID or policy code, never the rejected value |
Avoid accepting a caller-provided object and spreading it into response headers. Even a map with no newline characters can change Location, Set-Cookie, cache behavior, or content interpretation. Select approved fields individually and give sensitive fields their own builders. Keep the status code and any reason phrase application-owned too.
Validate after the intended decoding
RFC 9110, Section 5.5 calls field values containing CR, LF, or NUL invalid and dangerous. It requires a recipient to reject the message or replace those characters with spaces before further processing or forwarding. For a route constructing a new response, this guide recommends rejection of an invalid application value. That is a deliberately strict application policy.
A query string containing the printable characters %0d%0a is different from a string containing actual CR and LF. The dangerous transition can happen when URL decoding occurs after an earlier check. MITRE’s mitigation guidance recommends canonicalizing before validation and avoiding double decoding. Identify which router or parser owns that operation; do not call another decoder on a value already decoded by the framework.
The tag policy rejects percent signs as well as control characters, so both encoded and double-encoded separator fixtures are invalid for this particular field. Other fields may legitimately contain percent escapes. Define their grammar and transformation order instead of copying this narrow alphabet into a URL or filename feature. Do not repeatedly decode until a value stops changing.
Build the header plan before sending the response
This small Node.js helper returns one immutable, scalar-valued header plan:
import { validateHeaderValue } from 'node:http';
export function requestTagHeader(tag) {
if (typeof tag !== 'string' || tag.length === 0 || tag.length > 64 ||
/[^A-Za-z0-9_-]/.test(tag)) {
throw new TypeError('response_tag_invalid');
}
validateHeaderValue('X-Request-Tag', tag);
return Object.freeze({ 'X-Request-Tag': tag });
}
The helper rejects arrays, objects, numbers, empty strings, and unwanted characters without implicit string conversion. Its alphabet check also catches a trailing newline. The explicit validateHeaderValue call repeats Node’s low-level value check at the preparation stage; Node already applies that check through its HTTP APIs. It is useful here for making the timing visible, not evidence of an additional runtime vulnerability.
Call the helper before any writeHead, write, end, or flushHeaders operation that commits the response. Catch a policy rejection at that boundary, send a fixed 400 response with no tag field, and return immediately. Only the successful branch should apply the prepared header and perform actions that depend on an accepted tag. Do not export a generic helper that accepts an arbitrary name alongside this value.
For cookies and download metadata, use a maintained field-aware framework helper and review its documented grammar. A custom tag builder cannot establish those fields’ semantics. A header value can pass Node’s low-level check while still violating the application’s destination, session, or caching policy.
Review the last writer
Node documents that headers supplied to writeHead() take precedence over values previously set with setHeader(). A validated value can therefore be replaced by later middleware or a convenience response wrapper. Trace the final call order and test the emitted result. Checking getHeader() halfway through a handler does not prove what reaches the client.
Apply the same review to error handlers and proxies. If a proxy adds, decodes, or rewrites a field after application validation, its output becomes another validation boundary. Record the HTTP versions and actual transformation rules. An HTTP/2 client-facing connection alone does not establish how an HTTP/1.1 upstream response is constructed.
Happy-path operator workflow
- Inventory the outgoing fields, status selection, middleware, and proxy mappings touched by the patch.
- Assign each field a fixed name, input source, meaning, permitted grammar, size ceiling, and decoding owner.
- Prepare and validate all dynamic values before response commitment or dependent actions.
- Use structured response APIs with runtime validation enabled. Remove raw serialization fallbacks.
- Trace later writers, especially
writeHead()overrides, error middleware, and proxy transformations. - Run one known-good request and the invalid-value fixtures through the real route. Retain the final status, header names, body class, and action count.
Error-path operator workflow
Classify failures by origin. A caller-supplied tag that violates the route contract can receive a fixed 400. An invalid value created by trusted application code or received from an upstream service needs a controlled server or gateway failure appropriate to that route. Do not misclassify an upstream serialization defect as bad user input.
Reject before commitment whenever possible. Once headers have been sent, an error handler cannot safely replace the response with a fresh status and header set. Use the framework’s established stream-abort or connection-close behavior, record a bounded internal failure code, and repair the validation order. Never append a second status line to the body or retry the value through a lower-level socket writer.
Before commitment, remove any queued dynamic fields if the framework’s error path reuses the same response object. Return fixed error text, avoid echoing the invalid value, and ensure no dependent job, session change, or redirect occurred. Keep logs structured and limited to the field’s code-owned identifier, policy version, and failure class.
Build a harmless adversarial test matrix
Use an isolated local server with inert marker values. Inspect the final response without following redirects. Include a successful control so a disconnected server or broken fixture cannot masquerade as successful rejection.
| Fixture | Expected result under the tag policy |
|---|---|
job_42-Ab | 200 with exactly the expected tag value |
| Empty, missing, non-string, or 65-character value | Controlled rejection before tag-dependent work |
Actual CRLF followed by X-Fixture: marker | No injected field and no echoed tag |
| Actual CR alone, LF alone, or NUL | Controlled rejection |
| A trailing LF after a valid-looking tag | Rejected; no end-of-string matching loophole |
| Tab, space, or non-ASCII text | Rejected by this application’s narrow grammar |
| URL-encoded CRLF through the query parser | Decoded once, then rejected |
| Double-encoded CRLF through the same parser | Remaining percent escapes rejected by this policy |
| Later middleware tries to replace the value | Integration assertion detects the final unexpected value |
| Invalid value arrives after streaming starts | Existing error path terminates safely; no replacement response |
For a route-level test, assert status, the absence of X-Fixture and X-Request-Tag on rejection, a fixed body, and zero tag-dependent actions. Test the runtime’s separator rejection directly as a separate assertion; the tag allowlist rejects those values before Node’s serializer sees them. For a proxy deployment, inspect both the application response and the final client-visible response. Do not send these fixtures to public services.
The displayed helper was exercised in a disposable local harness on Node.js v24.22.1. Three tests passed: accepted and rejected tag values, core header-name/value rejection, and a loopback HTTP route using a single query-parser decoding step. The route test checked fixed rejection responses, absent injected fields, and an admission counter. Those results cover the helper and local route only. Proxy rewriting, late middleware overrides, and post-commit streaming behavior remain acceptance tests for the repository under review.
Failure modes
Checking the raw query before the router decodes it. The checked representation differs from the value sent to the header API. Validate the parsed application value and prevent a second decoding step.
Treating the runtime exception as the entire policy. Automatic validation protects syntax. It does not decide whether a caller may set a redirect, cookie, cache directive, or arbitrary field name.
Deleting CRLF and continuing. Ad hoc repair changes the value and can conceal a broken data path. For this application contract, reject and return a fixed error.
Validating early, then overwriting late. Middleware, writeHead(), and proxy rules can replace the approved value. Assert the final emitted headers.
Falling back to a raw response writer. A compatibility workaround can bypass the structured serializer’s checks. Keep one governed construction path.
Testing only the helper. The actual route may decode again, catch the error incorrectly, or send headers first. Test the route’s final response and dependent actions too.
FAQ
Does modern Node.js already block literal CRLF in headers?
Its HTTP documentation says invalid names and values throw and that validation occurs automatically. Preserve that behavior. The application still needs allowed names, field-specific semantics, controlled errors, and validation before response commitment.
Should every header use the example’s ASCII alphabet?
No. It is a contract for one inert tag. Apply the relevant field grammar and documented encoding for other headers. RFC 9110 permits field content beyond this example’s alphabet; a stricter application rule needs a product reason.
Is HTML escaping a header defense?
It addresses a different output grammar. Use the header’s own construction API and validation rules. Do not send an HTML-escaped value merely because it passed a template safety check.
Can a valid Location still be unsafe?
Yes. Syntax validation does not authorize the navigation destination. Apply an explicit destination policy before creating the response, using the linked open-redirect workflow.
Sources checked
Three primary references were fetched successfully on November 3, 2026 UTC:
- RFC 9110: HTTP Semantics , published by the RFC Editor, defines field names in Section 5.1 and invalid CR, LF, and NUL in field values in Section 5.5. This stable protocol reference supports the grammar claims.
- MITRE CWE-113 , published by the MITRE CWE Program, defines improper neutralization in outgoing HTTP headers, its consequences, and mitigation guidance on validation and decoding. The fetched entry reports an October 28, 2026 update.
- Node.js HTTP documentation
, published by the Node.js project, documents automatic validation,
validateHeaderName,validateHeaderValue, response commitment, andwriteHeadprecedence. The fetched page identifies Node.js v26.13.0; the local helper test used v24.22.1.
These maintained technical references support an evergreen patch-review workflow. The contract, example limits, error policy, and test matrix are recommendations. They do not establish a production vulnerability or certify a deployment.
Reader next step
Choose one outgoing header changed by the next coding-agent patch. Write its name, meaning, input source, decoding owner, grammar, and rejection behavior. Add one valid control plus CR, LF, NUL, encoded-separator, and over-limit fixtures to the actual route. Keep the patch open until the final response matches that contract and rejection occurs before any dependent action or response commitment.