{"vulnerability": "cve-2026-82417", "sightings": [{"uuid": "416880bf-f8b7-433a-a698-f692d0dc4439", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "86ecb4e1-bb32-44d5-9f39-8a4673af8385", "vulnerability": "CVE-2026-82417", "type": "published-proof-of-concept", "source": "https://github.com/ljharb/qs/security/advisories/GHSA-4mjr-xmp4-gh2g", "content": "", "creation_timestamp": "2026-09-02T16:35:31.372723Z"}, {"uuid": "f97a9dbc-3235-4817-a280-01b8b63af666", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-82417", "type": "seen", "source": "https://gist.github.com/tomasantunes/b27c1462edf01f28a31f453280d86577", "content": "# Security review of the Express checkout\n\nDate: 2026-09-28\n\n## Summary and scope\n\nThe strongest findings are in the bundled example applications: reflected and stored HTML injection, shared-prototype modification, unauthenticated record updates, plaintext password logging, unvalidated redirects, and a public cookie-signing key. These matter if the examples are exposed or copied into a real application. They do **not** establish that applications merely depending on Express are vulnerable.\n\nOne already-reported framework behavior permits view lookup outside the configured views directory when an application supplies an untrusted template name. Two known dependency advisories also deserve attention, but their exploit prerequisites were not found in the default Express code paths reviewed. **This review did not establish a new, independently exploitable vulnerability in Express core.**\n\nThe local [package.json](C:/Users/tomas/Documents/ChatGPT/express-master/package.json) declares version 5.2.1. This is a source snapshot without Git metadata, so it cannot be identified as an exact upstream commit or assumed identical to the published 5.2.1 package. There is no local node_modules directory or lockfile; [.npmrc](C:/Users/tomas/Documents/ChatGPT/express-master/.npmrc) explicitly disables package-lock creation. Exact installed/transitive dependency versions could not be audited.\n\nReview covered the framework's request/response, rendering, configuration, query parsing and file-serving wrappers; example request handlers and templates; relevant regression tests; dependency declarations; and workflow configuration. No application build, dependency installation, full test suite, live HTTP exploit, or Redis test was performed. Existing source files were not edited. Small Node.js probes evaluated actual source in isolated VM contexts, with missing dependencies replaced by explicitly limited mocks. Those checks validate individual code paths, not end-to-end exploitability.\n\nSeverity below is an assessment of the stated scenario, not an upstream CVSS score. Demo-only data limits the actual impact in this checkout. Higher production impact requires reuse with real users or data.\n\n| ID | Finding | Scope | Assessed impact | Evidence |\n| --- | --- | --- | --- | --- |\n| F1 | Reflected XSS in vhost response | Example | Medium if exposed | Handler output verified |\n| F2 | Stored XSS through online user-agent identifiers | Optional example | Medium if operational | HTML helper verified; Redis untested |\n| F3 | User identifier selects a shared prototype for writes | Examples | Medium, limited property corruption | Array.prototype mutation verified |\n| F4 | Record changes require no authentication or ownership check | Examples | Low with demo data; potentially high after reuse | Controller mutation verified; routes inspected |\n| F5 | Authentication logs plaintext passwords | Example | Medium when real credentials are submitted | Direct source evidence |\n| F6 | Request Referrer controls redirect destination | Examples | Low; phishing/navigation risk | Controller data flow verified |\n| F7 | Public signing key permits cookie-session forgery | Example configuration | Low for demo counter | Static evidence and middleware documentation |\n| C1 | View lookup can leave configured views directory | Conditional core API misuse | Potentially medium; template-dependent | Actual lookup verified; already reported |\n| D1/D2 | Allowed qs versions include known vulnerable releases | Conditional dependency exposure | Unconfirmed locally | Manifest and upstream advisories |\n\n## F1 \u2014 Reflected XSS in the vhost example\n\n**Location:** [examples/vhost/index.js:29](C:/Users/tomas/Documents/ChatGPT/express-master/examples/vhost/index.js:29), with HTML response behavior at [lib/response.js:137](C:/Users/tomas/Documents/ChatGPT/express-master/lib/response.js:137). CWE-79.\n\n**Description:** The main virtual host's `/:sub` route concatenates the decoded route parameter directly into `res.send()`. String responses default to HTML. An attacker-controlled path therefore becomes executable markup rather than displayed text.\n\n**Exploit overview:** On a locally running copy configured to serve `example.com`, request a path containing URL-encoded harmless test markup:\n\n```http\nGET /%3Csvg%20onload%3Dalert(1)%3E HTTP/1.1\nHost: example.com:3000\n```\n\nThe handler emits `requested `. A victim who opens the corresponding URL on the served hostname can execute the markup in that origin, absent a blocking browser policy. The Host condition matters: visiting the example through an unrelated hostname does not select this virtual host. This is URL-path injection, not an assumption that browsers can be forced to send arbitrary Host headers.\n\n**Validation:** The actual registered handler was invoked with a decoded parameter in an isolated VM; its raw output matched the string above. The existing vhost acceptance test establishes the intended `/foo` route and Host combination. Router decoding and browser execution were not run.\n\n**Protection:** Return this informational response as text/plain, or HTML-escape the parameter before composing HTML. Add a regression test for encoded markup. Keep the example isolated from production origins.\n\n**Prior-report check:** No exact matching issue was located in the searches described below; novelty is unverified.\n\n## F2 \u2014 Stored XSS through online user-agent identifiers\n\n**Locations:** [examples/online/index.js:32](C:/Users/tomas/Documents/ChatGPT/express-master/examples/online/index.js:32), [line 40](C:/Users/tomas/Documents/ChatGPT/express-master/examples/online/index.js:40), and [line 50](C:/Users/tomas/Documents/ChatGPT/express-master/examples/online/index.js:50). CWE-79.\n\n**Description:** Middleware submits the untrusted User-Agent header to the online tracker. The page later concatenates returned identifiers into HTML list items without escaping. This creates a stored HTML injection path if the tracker preserves those identifiers as expected.\n\n**Exploit overview:** Submit one request with `User-Agent: `. A subsequent visitor to `/` receives that value as markup if it appears in the tracker results. Exposure lasts while the attacker-controlled identifier remains in the returned online list; it is not necessarily permanent storage.\n\n**Impact and prerequisites:** Script execution in a viewer's origin. This optional example requires separately supplied `online`, Redis client, and Redis service components; those are absent here. The injection sink is confirmed, but compatibility, storage behavior, and the complete cross-request chain remain unverified.\n\n**Validation:** The real `list()` helper returned `\n\n` for an attacker-controlled identifier. Dependencies were mocked, so this is not a Redis integration test.\n\n**Protection:** Encode identifiers for HTML text, or render with an escaping template. Track stable server-assigned user identifiers instead of treating raw User-Agent strings as identities.\n\n**Prior-report check:** No exact matching issue was located; novelty is unverified.\n\n## F3 \u2014 Inherited user identifiers permit shared-prototype modification\n\n**Locations:** [examples/route-separation/user.js:14](C:/Users/tomas/Documents/ChatGPT/express-master/examples/route-separation/user.js:14), [line 40](C:/Users/tomas/Documents/ChatGPT/express-master/examples/route-separation/user.js:40), and [route registration](C:/Users/tomas/Documents/ChatGPT/express-master/examples/route-separation/index.js:41). The same pattern occurs in the [MVC user controller](C:/Users/tomas/Documents/ChatGPT/express-master/examples/mvc/controllers/user/index.js:11) and [pet controller](C:/Users/tomas/Documents/ChatGPT/express-master/examples/mvc/controllers/pet/index.js:11). CWE-1321.\n\n**Description:** The user loader indexes an ordinary array with an arbitrary path parameter and only tests whether the result is truthy. `users['__proto__']` returns `Array.prototype`. The update handler then writes attacker-supplied name and email values to that shared prototype.\n\n**Exploit overview:** Against a disposable local route-separation example, send:\n\n```http\nPUT /user/__proto__/edit HTTP/1.1\nHost: localhost:3000\nContent-Type: application/x-www-form-urlencoded\n\nuser[name]=audit-marker&amp;user[email]=audit%40example.invalid\n```\n\nThe URL-encoded parser is configured with `extended: true`, so the body supplies the expected nested `user` fields. The dangerous identifier comes from the route, not a special body-parser prototype key. After the handler runs, otherwise unrelated arrays inherit the injected `name` and `email` unless they define their own values.\n\n**Impact limits:** This demonstrates modification of `Array.prototype`, **not** arbitrary `Object.prototype` pollution. Writable fields are constrained by the controller. No remote-code-execution or authentication-bypass chain was established. The MVC variants write `name` on a similarly selected prototype; those variants were inspected, not executed.\n\n**Validation:** Executing the real load and update functions in a VM confirmed that fresh arrays inherited both marker fields, while `({}).name` remained undefined. Mutation stayed inside the disposable VM context.\n\n**Protection:** Require canonical nonnegative integer IDs, bounds-check them, and confirm own record membership before use. A Map with explicit keys also avoids inherited-property lookup. Authentication alone does not repair this lookup defect.\n\n**Prior-report check:** No exact matching report was located. This differs from the withdrawn Express query-property advisory discussed below: here the controller demonstrably writes a shared prototype.\n\n## F4 \u2014 Example record updates lack authentication and ownership checks\n\n**Locations:** [route-separation routes](C:/Users/tomas/Documents/ChatGPT/express-master/examples/route-separation/index.js:38), [update controller](C:/Users/tomas/Documents/ChatGPT/express-master/examples/route-separation/user.js:40), [MVC user update](C:/Users/tomas/Documents/ChatGPT/express-master/examples/mvc/controllers/user/index.js:36), and [MVC pet creation](C:/Users/tomas/Documents/ChatGPT/express-master/examples/mvc/controllers/user-pet/index.js:12). CWE-306/CWE-862.\n\n**Description:** These illustrative CRUD handlers select records by URL ID and modify them without authenticating a caller or checking record ownership. Loading `req.user` from a supplied ID is record retrieval; it is not proof of the caller's identity.\n\n**Exploit overview:** Send the F3-shaped PUT request to `/user/0/edit` with ordinary test name/email values and no Cookie or Authorization header. The selected user's fields are changed. The MVC example similarly accepts updates and pet creation without a caller-ownership check.\n\n**Impact and limits:** In this repository the records are explicitly fake and in-memory; no real customer account compromise is demonstrated. Reusing these routes with persistent records would expose data integrity. Cross-site form submission is an additional concern where method override is enabled, but no victim session is needed for the demonstrated direct-write problem.\n\n**Validation:** The actual route-separation load/update functions changed an ordinary record without authentication context. Route and middleware inspection found no authentication layer in that example. No live HTTP request was made.\n\n**Protection:** Keep demo servers private. In reused code, authenticate callers and authorize each record operation before mutation; add CSRF defenses where cookie-authenticated forms are supported. Validate submitted fields and enforce IDs independently of F3.\n\n**Prior-report check:** No exact matching report was located. This is a deployment/reuse risk in instructional code, not an Express core authorization bypass.\n\n## F5 \u2014 Authentication example logs plaintext passwords\n\n**Location:** [examples/auth/index.js:61](C:/Users/tomas/Documents/ChatGPT/express-master/examples/auth/index.js:61). CWE-532.\n\n**Description:** `authenticate()` logs both username and submitted password before checking the account when the example runs as the main module. This includes failed login attempts and mistyped passwords. Password hashing later in the function does not remove the plaintext already written to stdout.\n\n**Exploit overview:** Submit a login containing a unique dummy password to a standalone local example, then inspect its process output. The expected line includes that password. An attacker who separately gains read access to collected logs could recover passwords users submitted, including credentials mistakenly entered for another service.\n\n**Prerequisites and limits:** The log branch requires `!module.parent`; importing the example as a module, as the acceptance tests do, skips it. Log access is an additional prerequisite for disclosure. The example only defines a demo account, so real credential impact requires real users to submit secrets.\n\n**Validation:** Direct static evidence; no authentication service or password-hashing dependency was run.\n\n**Protection:** Remove password values from authentication logs and redact request bodies at log collection boundaries. If reused with real credentials, address retention and access to already collected logs.\n\n**Prior-report check:** No exact matching issue was located; novelty is unverified.\n\n## F6 \u2014 Unvalidated Referrer redirects in examples\n\n**Locations:** [examples/cookies/index.js:36](C:/Users/tomas/Documents/ChatGPT/express-master/examples/cookies/index.js:36), [line 46](C:/Users/tomas/Documents/ChatGPT/express-master/examples/cookies/index.js:46), [examples/auth/index.js:119](C:/Users/tomas/Documents/ChatGPT/express-master/examples/auth/index.js:119), and [examples/route-separation/user.js:46](C:/Users/tomas/Documents/ChatGPT/express-master/examples/route-separation/user.js:46). CWE-601.\n\n**Description:** The examples redirect to `req.get('Referrer') || '/'` without restricting the destination. That request header is not a trusted navigation target. Express supports both header spellings and gives the nonstandard `Referrer` spelling precedence at [lib/request.js:73](C:/Users/tomas/Documents/ChatGPT/express-master/lib/request.js:73).\n\n**Exploit overview:** Send `GET /forget` to the cookies example with `Referer: https://audit.example.invalid/`. The response is expected to set that external destination in Location. For a browser scenario, a navigation or form submission initiated on an attacker-controlled site can supply its own origin as Referer when referrer policy permits. This requires no ability to set an arbitrary browser-forbidden header. The auth variant additionally requires successful login; the update variant requires a valid update body.\n\n**Impact:** Untrusted navigation and possible phishing using a trusted site's redirect response. A raw-header reproduction alone does not establish an OAuth-token leak, account takeover, or browser-executable `javascript:` redirect.\n\n**Validation:** The actual route-separation controller passed an external marker URL unchanged to a redirect stub. Other occurrences and the core header/redirect implementations were inspected.\n\n**Protection:** Prefer a fixed destination or a server-controlled return-path identifier. If a URL is necessary, parse it against a configured canonical origin, check the resulting origin, and emit an approved local path. Do not derive the trusted origin from the request Host header.\n\n**Prior-report check:** [Issue #3951](https://github.com/expressjs/express/issues/3951) already discusses the Referrer/Referer precedence and spoofability. [Issue #5581](https://github.com/expressjs/express/issues/5581) discusses historical redirect-back dependence on that header. These are related reports, not proof that the exact example vulnerability was filed. This is distinct from the patched malformed-URL advisory.\n\n## F7 \u2014 Public signing secret defeats cookie-session integrity\n\n**Location:** [examples/cookie-sessions/index.js:13](C:/Users/tomas/Documents/ChatGPT/express-master/examples/cookie-sessions/index.js:13). CWE-321.\n\n**Description:** The example signs client-side session data using the fixed, published secret `manny is cool`. Anyone reading the source knows the integrity key. Cookie-session stores session contents client-side and protects them with a signature, as its [upstream documentation](https://github.com/expressjs/cookie-session) explains.\n\n**Exploit overview:** Serialize a session with a chosen numeric `count`, generate its valid cookie signature using the public key and the middleware's signing format, and send the session and signature cookies to `/`. The example should accept the chosen state and increment that count. This requires no brute force. Signature generation and cookie acceptance were not executed in this review.\n\n**Impact limits:** The actual example only displays a counter, so its direct integrity impact is low. Copying the same key into a client-side authentication session would create a much more serious problem, but this repository does not demonstrate that scenario. The separate `express-session` examples also publish demo secrets; knowing those keys alone does not fabricate an authenticated server-side session record.\n\n**Protection:** Supply a deployment-specific cryptographically random secret outside source control, rotate exposed keys, and invalidate affected sessions. Never use client-side session fields as trusted claims when the key is public.\n\n**Prior-report check:** No exact matching issue was located. This is example configuration, not a cryptographic flaw in Express or cookie-session.\n\n## C1 \u2014 Already-reported view-path containment concern\n\n**Locations:** [lib/view.js:104](C:/Users/tomas/Documents/ChatGPT/express-master/lib/view.js:104), [engine selection](C:/Users/tomas/Documents/ChatGPT/express-master/lib/view.js:55), and [absolute-path tests](C:/Users/tomas/Documents/ChatGPT/express-master/test/app.render.js:10). CWE-22, conditional on unsafe application input.\n\n**Description:** `View.lookup()` resolves a template name against each views root but does not enforce that the resulting path stays inside it. Absolute paths and parent-directory references can therefore select files elsewhere. Absolute-path support is intentional and covered by existing tests, so the views setting must not be treated as a sandbox.\n\n**Exploit overview:** If an application defines a route that directly calls `res.render(req.params.name)`, an encoded name such as `..%2Fprivate.ejs` can select a sibling template outside its views directory. A readable file compatible with the selected engine must exist there. The review verified this using the existing `test/fixtures/user.tmpl` file with `test/fixtures/local_layout` as the configured root; lookup returned the file above that root.\n\n**Limits:** No supplied example was found to expose that exact untrusted-template-name route. A default engine appends an extension to extensionless names; requesting an extensionless operating-system file does not automatically disclose it. Arbitrary file disclosure and code execution cannot be assumed merely from path resolution. Executing an attacker-written template would require an additional file-write capability and compatible engine behavior.\n\n**Protection:** Map external page IDs to a fixed set of internal template names. If dynamic paths are unavoidable, enforce containment and approved extensions, including canonical-path/symlink considerations. Avoid relying on a bare string-prefix check.\n\n**Prior-report check:** This behavior is already reported in [Express issue #7140](https://github.com/expressjs/express/issues/7140), shown open when checked. Its existence is not a newly discovered core vulnerability. The [Express threat model](https://github.com/expressjs/security-wg/blob/main/docs/ThreatModel.md) also makes application input validation relevant to classification.\n\n## D1/D2 \u2014 Known qs advisories: permitted versions, conditional reachability\n\nThe manifest declares `qs: ^6.15.2`, allowing both affected releases and patched 6.16.0 or later 6.x releases. No installed tree exists here, so a vulnerable resolved version is **not confirmed**. [Express issue #7439](https://github.com/expressjs/express/issues/7439), although filed against Express 4.22.2, already names both advisories below.\n\n### D1 \u2014 CVE-2026-82417: exception during qs serialization\n\nThe [qs maintainer advisory](https://github.com/ljharb/qs/security/advisories/GHSA-4mjr-xmp4-gh2g) lists affected versions 2.2.5 through 6.15.3 and a fix in 6.16.0. An object carrying a non-callable `constructor.isBuffer` can trigger a TypeError in `qs.stringify()`.\n\n**Exploit overview:** On an affected release, a permissive parse of `x[constructor][isBuffer]=y` followed by serialization can trigger the exception. A process-level denial of service additionally needs an uncaught execution context; a caught route error is not automatically worker termination.\n\n**Local applicability:** Express's optional extended query parser enables `allowPrototypes`, but no `qs.stringify()` sink was found in the reviewed framework or examples. The default query parser is simple. This is a dependency/reuse risk, not a demonstrated remote crash of this checkout.\n\n### D2 \u2014 CVE-2026-82562: array-limit bypass with comma parsing\n\nThe [qs maintainer advisory](https://github.com/ljharb/qs/security/advisories/GHSA-x5fp-wj9c-mxmx) lists affected versions 6.14.2 through 6.15.3 and a fix in 6.16.0. With `comma: true`, bracket-push values can bypass configured array limits.\n\n**Exploit overview:** A small diagnostic input is `a[]=1,2,3,4` with `arrayLimit: 3` and limit exceptions enabled; vulnerable parsing can accept it. Larger accepted inputs can allocate more memory than intended, within any transport/body-size limits.\n\n**Local applicability:** The core extended-query wrapper does not enable comma parsing, and no custom parser enabling it was found in the examples. Dependency code was not installed or executed, so the advisory was not reproduced locally.\n\n**Protection for both:** In an actual deployed dependency tree, verify all qs instances and resolve them to 6.16.0 or a later release fixing these advisories. Retain bounded request sizes, conservative parser options, and proper error handling. Raising the declared minimum would prevent reuse of these affected versions; no manifest change was made here.\n\n## Upstream checks and findings deliberately not claimed\n\nThe [Express issue tracker](https://github.com/expressjs/express/issues) and [security advisories](https://github.com/expressjs/express/security/advisories) were checked on the review date. Searches included vhost XSS, online XSS, route-separation security/prototype pollution, password logging, cookie-session secrets, redirects, and view containment. Some direct filtered GitHub pages failed to load; indexed search and individually accessible issues were used. Therefore, \u201cno matching issue located\u201d does **not** mean \u201cnever reported,\u201d and private disclosures cannot be checked.\n\n| Existing report | Assessment for this checkout |\n| --- | --- |\n| [#7140: view containment](https://github.com/expressjs/express/issues/7140) | Same behavior confirmed; documented as C1 with application prerequisites. |\n| [#3951: Referrer precedence](https://github.com/expressjs/express/issues/3951) | Related to F6; the header is not an authorization or destination-validation mechanism. |\n| [#7439: qs dependency reports](https://github.com/expressjs/express/issues/7439) | Relevant allowed versions; D1/D2 explain why actual version and reachable options/sinks matter. |\n| [#7473: proxy-addr update request](https://github.com/expressjs/express/issues/7473) | Requests moving from 2.0.7 to 2.0.8; this manifest already requires ^2.0.8. Not counted as a confirmed finding. |\n| [GHSA-qw6h-vgh9-j6wx: redirect XSS](https://github.com/expressjs/express/security/advisories/GHSA-qw6h-vgh9-j6wx) | Published fixes include Express 5.0.0. Local redirect HTML escapes the address and omits the old clickable link; not reported as a current defect. |\n| [GHSA-rv95-896h-c2vc: malformed redirect URLs](https://github.com/expressjs/express/security/advisories/GHSA-rv95-896h-c2vc) | Historical advisory, not evidence of a current bypass. F6 instead concerns example code deliberately accepting an external destination. |\n| [GHSA-pj86-cfqh-vqx6: query properties](https://github.com/expressjs/express/security/advisories/GHSA-pj86-cfqh-vqx6) | Explicitly withdrawn/rejected upstream as a security vulnerability. Not counted merely because `allowPrototypes` appears in source. |\n\nOther reviewed boundaries: core application caches/settings use null-prototype objects; trust proxy defaults to false; sendFile/download delegate to send, with existing traversal-rejection tests for configured roots. These observations are not an assurance that all possible configurations are safe. Missing generic security headers, intentional JSONP support, and dependency ranges alone were not treated as independently exploitable bugs.\n\nAdditional demo exposure: the [auth example](C:/Users/tomas/Documents/ChatGPT/express-master/examples/auth/index.js:50) deliberately provisions `tj` / `foobar` and advertises those credentials after failure. Anyone can use them to obtain the example's restricted-page session if it is exposed. This is intentional demo behavior, not a separate new authentication vulnerability. It must be removed before adapting the example for real access control.\n\n## Validation record and recommended order\n\nSix dependency-free checks passed under Node.js v25.9.0: vhost raw output, online list raw output, route-separation Array.prototype modification, unauthenticated ordinary-record mutation, unvalidated redirect forwarding, and actual View lookup outside its configured root. No network listener was started. The view probe used the real filesystem and stubbed only debug logging/engine registration; example probes used mocked Express/dependencies and real handler functions. HTTP parsing, browser behavior, session signature acceptance, template execution, and Redis persistence remain untested.\n\nRecommended order if adapting or deploying this code:\n\n1. Keep instructional servers off public/production origins; remove demo credentials and keys.\n2. Fix output encoding (F1/F2), inherited record lookup (F3), and authorization (F4).\n3. Remove password logging and restrict redirects (F5/F6); rotate reused secrets (F7).\n4. Audit real application render call sites for C1 and resolve the installed dependency tree for D1/D2.\n\nOnly this report was added. No proposed remediation was applied to project code.\n", "creation_timestamp": "2026-09-28T18:39:22.000000Z"}]}