{"record":{"id":"b0eb802bf3b17fb6","repo":"denoland/deno","slug":"err-invalid-char-b0eb80","errorCode":"ERR_INVALID_CHAR","errorMessage":"Invalid character in header content [\"Link\"]","messagePattern":"Invalid character in header content \\[\"Link\"\\]","errorType":"error_code","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"ext/node/polyfills/_http_server.js","lineNumber":473,"sourceCode":"  hints,\n  cb,\n) {\n  let head = \"HTTP/1.1 103 Early Hints\\r\\n\";\n\n  validateObject(hints, \"hints\");\n\n  if (hints.link === null || hints.link === undefined) {\n    return;\n  }\n\n  const link = validateLinkHeaderValue(hints.link);\n\n  if (link.length === 0) {\n    return;\n  }\n\n  if (checkInvalidHeaderChar(link)) {\n    throw new ERR_INVALID_CHAR(\"header content\", \"Link\");\n  }\n\n  head += \"Link: \" + link + \"\\r\\n\";\n\n  const keys = ObjectKeys(hints);\n  for (let i = 0; i < keys.length; i++) {\n    const key = keys[i];\n    if (key !== \"link\") {\n      validateHeaderName(key);\n      const value = hints[key];\n      validateHeaderValue(key, value);\n      head += key + \": \" + value + \"\\r\\n\";\n    }\n  }\n\n  head += \"\\r\\n\";\n\n  this._writeRaw(head, \"ascii\", cb);","sourceCodeStart":455,"sourceCodeEnd":491,"githubUrl":"https://github.com/denoland/deno/blob/9ad36f7a2cce60488e6ec52283efb32efddaf93a/ext/node/polyfills/_http_server.js#L455-L491","documentation":"ERR_INVALID_CHAR thrown while composing an Early Hints response: the link hint is normalized by validateLinkHeaderValue(), then re-checked with checkInvalidHeaderChar(). If the resulting value still contains CR, LF, or other characters illegal in a header, the write is refused with 'Invalid character in header content [\"Link\"]' — this is the response-splitting defense for the Link header.","triggerScenarios":"res.writeEarlyHints({ link: `</assets/${req.query.file}>; rel=preload` }) where the interpolated value contains %0d%0a (decoded to CR LF), a raw newline from config, or other control bytes.","commonSituations":"Interpolating request parameters or config values into preload hints; Link values copied from multi-line YAML strings; penetration tests probing header injection through Early Hints.","solutions":["Strip CR/LF and control characters from every dynamic segment before building the Link value","Restrict dynamic parts to an allowlist pattern such as /^[\\w.-]+$/ for filenames","Encode or reject unsafe characters instead of passing them through"],"exampleFix":"// before\nres.writeEarlyHints({ link: `</assets/${req.query.css}>; rel=preload` }); // css contains %0d%0a\n\n// after\nconst css = String(req.query.css).replace(/[\\r\\n\\u0000-\\u001f]/g, '');\nres.writeEarlyHints({ link: `</assets/${css}>; rel=preload` });","handlingStrategy":"validation","validationCode":"const sanitizeHeaderValue = (s) =>\n  String(s).replace(/[\\r\\n\\u0000-\\u001f\\u007f]/g, '');","typeGuard":"const isSafeHeaderValue = (s) =>\n  !/[\\r\\n\\u0000-\\u001f\\u007f]/.test(String(s));","tryCatchPattern":"try {\n  res.writeEarlyHints({ link });\n} catch (e) {\n  if (e.code === 'ERR_INVALID_CHAR') return; // skip unsafe hint, keep serving\n  throw e;\n}","preventionTips":["Treat any string flowing into a header as untrusted","Build Link values from constants plus strictly validated dynamic parts","Add a shared sanitize step for every header you compose from user input"],"tags":["http","early-hints","header-injection","security","node-compat"],"backgroundTag":"http-header-injection","analyzedSha":"9ad36f7a2cce60488e6ec52283efb32efddaf93a","analyzedAt":"2026-08-20T13:07:44.778Z","contentChangedAt":"2026-08-20T13:07:44.778Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}