Action not permitted
Modal body text goes here.
Modal Title
Modal Body
Vulnerability from cleanstart
Package langfuse-worker version 3.214.0-r1 fixes 28 vulnerabilities: ghsa-frvp-7c67-39w9, ghsa-p63j-vcc4-9vmv, ghsa-55q2-fjhq-7xh7, ghsa-c2j3-45gr-mqc4, CVE-2026-69192...
| URL | Type | |
|---|---|---|
{
"affected": [
{
"package": {
"ecosystem": "Alpine",
"name": "langfuse-worker"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.214.0-r1"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"3.214.0-r1"
]
}
],
"credits": [],
"database_specific": {},
"details": "Package langfuse-worker version 3.214.0-r1 fixes 28 vulnerabilities: ghsa-frvp-7c67-39w9, ghsa-p63j-vcc4-9vmv, ghsa-55q2-fjhq-7xh7, ghsa-c2j3-45gr-mqc4, CVE-2026-69192...",
"id": "CLEANSTART-2026-PY30601",
"modified": "2026-09-02T06:40:33Z",
"published": "2026-09-01T11:17:16Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/langfuse/langfuse"
}
],
"related": [],
"schema_version": "1.7.3",
"summary": "Security fixes in langfuse-worker 3.214.0-r1",
"upstream": [
"ghsa-frvp-7c67-39w9",
"ghsa-p63j-vcc4-9vmv",
"ghsa-55q2-fjhq-7xh7",
"ghsa-c2j3-45gr-mqc4",
"CVE-2026-69192",
"CVE-2026-54272",
"CVE-2026-69198",
"ghsa-5p4m-2wfm-xmqj",
"CVE-2026-50159",
"CVE-2026-71436",
"CVE-2026-71437",
"CVE-2026-71439",
"CVE-2026-71438",
"CVE-2026-67213",
"CVE-2026-67214",
"ghsa-7rqj-j65f-68wh",
"ghsa-xmf8-cvqr-rfgj",
"ghsa-x445-f3h2-j279",
"CVE-2026-73419",
"CVE-2026-73418",
"CVE-2026-73420",
"CVE-2026-69153",
"ghsa-f88m-g3jw-g9cj",
"CVE-2026-13697",
"CVE-2026-14643",
"CVE-2026-15157",
"CVE-2026-16728",
"CVE-2026-16729"
]
}
GHSA-5P4M-2WFM-XMQJ
Vulnerability from github – Published: 2026-08-06 20:27 – Updated: 2026-08-06 20:27Quadratic CPU consumption in !!omap resolution (js-yaml 3.x and 4.x)
Summary
resolveYamlOmap() enforces key uniqueness for !!omap sequences with a linear
scan (objectKeys.indexOf(...)) inside the per-element loop, making resolution
O(n²) in the number of entries. A modestly sized YAML document therefore
consumes disproportionate CPU inside yaml.load(), giving a denial of service
against any consumer that parses untrusted YAML.
!!omap is registered in the default schema
(lib/schema/default.js → require('../type/omap')), so a plain
yaml.load(untrustedInput) with no options is affected — no custom schema or
non-default configuration is required.
This is the same weakness as CVE-2026-59870 / GHSA-724g-mxrg-4qvm, which was fixed in the 5.x line in 5.2.1. That fix was never backported: both currently maintained legacy lines still carry the original implementation.
Affected versions
| Line | Latest tested | Status |
|---|---|---|
| 3.x | 3.15.0 | Affected — objectKeys.indexOf(pairKey) at lib/type/omap.js:29 |
| 4.x | 4.3.0 | Affected — objectKeys.indexOf(pairKey) at lib/type/omap.js:30 |
| 5.x | 5.2.2 | Not affected — fixed in 5.2.1 (uses a Set) |
Both figures are the newest release of each line at the time of writing, so this is not a "you are on an old version" issue.
Details
lib/type/omap.js (js-yaml 4.3.0):
if (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey)
else return false
objectKeys grows by one element per entry, and Array.prototype.indexOf is a
linear scan, so resolving an n-entry !!omap performs roughly
1 + 2 + … + n comparisons — quadratic in n. The work happens synchronously
inside yaml.load(), blocking the event loop for its whole duration.
The 5.x line already solves exactly this by tracking seen keys in a Set
(src/tag/sequence/omap.ts):
if (carrier.seen.has(key)) return 'duplicate key in ordered map'
carrier.seen.add(key)
Proof of concept
// poc.js — node poc.js
const yaml = require('js-yaml');
const doc = n => '!!omap\n' + Array.from({length: n}, (_, i) => `- k${i}: ${i}`).join('\n') + '\n';
for (const n of [10000, 20000, 40000, 80000]) {
const d = doc(n), t = Date.now();
yaml.load(d); // default schema, no options
console.log(`n=${n} bytes=${d.length} load=${Date.now() - t}ms`);
}
Measured (node v20.20.2, default heap, no flags)
js-yaml 4.3.0
n=10000 bytes=137787 load=54ms
n=20000 bytes=297787 load=169ms
n=40000 bytes=617787 load=646ms
n=80000 bytes=1257787 load=2607ms
js-yaml 3.15.0
n=10000 bytes=137787 load=53ms
n=20000 bytes=297787 load=166ms
n=40000 bytes=617787 load=641ms
n=80000 bytes=1257787 load=2567ms
Runtime grows by a factor of ~4 for each doubling of n, which is the
signature of O(n²) (linear growth would be ~2×).
Scaling further: a 2.48 MB document with 150,000 entries blocked
yaml.load() for 10.8 seconds.
Impact
Any service that parses attacker-influenced YAML with js-yaml 3.x or 4.x can be stalled with a small input. Because the loop is synchronous, a single request blocks the Node.js event loop and stalls every other request in the process — so the amplification is per-process, not just per-request.
Suggested severity: consistent with CVE-2026-59870 (the same weakness in 5.x), i.e. Availability-only impact, network attack vector, no privileges or user interaction required.
Suggested fix
Mirror the 5.x fix — replace the linear scan with a Set:
// lib/type/omap.js
const seen = new Set()
// ...
if (seen.has(pairKey)) return false
seen.add(pairKey)
This preserves the existing duplicate-key rejection semantics exactly while
making resolution O(n). A maxOmapLength-style cap would also work, but the
Set matches what 5.x already ships and requires no new option.
References
- CVE-2026-59870 / GHSA-724g-mxrg-4qvm — same weakness in 5.0.0–5.2.0, fixed in 5.2.1
lib/type/omap.js(3.x, 4.x) — the affected resolverlib/schema/default.js— registers!!omapin the default schema
Discovery
Found by an automated static-analysis and executed-proof-of-concept scanner run against js-yaml 4.2.0, then manually verified against 3.15.0 and 4.3.0 by executing the proof of concept above. All timings in this report were measured on the current releases of each line, not on the version originally scanned.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.3.1"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "js-yaml"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.15.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-407"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-06T20:27:32Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "# Quadratic CPU consumption in `!!omap` resolution (js-yaml 3.x and 4.x)\n\n## Summary\n\n`resolveYamlOmap()` enforces key uniqueness for `!!omap` sequences with a linear\nscan (`objectKeys.indexOf(...)`) inside the per-element loop, making resolution\n**O(n\u00b2)** in the number of entries. A modestly sized YAML document therefore\nconsumes disproportionate CPU inside `yaml.load()`, giving a denial of service\nagainst any consumer that parses untrusted YAML.\n\n`!!omap` is registered in the **default schema**\n(`lib/schema/default.js` \u2192 `require(\u0027../type/omap\u0027)`), so a plain\n`yaml.load(untrustedInput)` with no options is affected \u2014 no custom schema or\nnon-default configuration is required.\n\n**This is the same weakness as CVE-2026-59870 / GHSA-724g-mxrg-4qvm**, which was\nfixed in the 5.x line in 5.2.1. That fix was never backported: both currently\nmaintained legacy lines still carry the original implementation.\n\n## Affected versions\n\n| Line | Latest tested | Status |\n|---|---|---|\n| 3.x | **3.15.0** | Affected \u2014 `objectKeys.indexOf(pairKey)` at `lib/type/omap.js:29` |\n| 4.x | **4.3.0** | Affected \u2014 `objectKeys.indexOf(pairKey)` at `lib/type/omap.js:30` |\n| 5.x | 5.2.2 | **Not affected** \u2014 fixed in 5.2.1 (uses a `Set`) |\n\nBoth figures are the newest release of each line at the time of writing, so\nthis is not a \"you are on an old version\" issue.\n\n## Details\n\n`lib/type/omap.js` (js-yaml 4.3.0):\n\n```js\nif (objectKeys.indexOf(pairKey) === -1) objectKeys.push(pairKey)\nelse return false\n```\n\n`objectKeys` grows by one element per entry, and `Array.prototype.indexOf` is a\nlinear scan, so resolving an `n`-entry `!!omap` performs roughly\n`1 + 2 + \u2026 + n` comparisons \u2014 quadratic in `n`. The work happens synchronously\ninside `yaml.load()`, blocking the event loop for its whole duration.\n\nThe 5.x line already solves exactly this by tracking seen keys in a `Set`\n(`src/tag/sequence/omap.ts`):\n\n```ts\nif (carrier.seen.has(key)) return \u0027duplicate key in ordered map\u0027\ncarrier.seen.add(key)\n```\n\n## Proof of concept\n\n```js\n// poc.js \u2014 node poc.js\nconst yaml = require(\u0027js-yaml\u0027);\nconst doc = n =\u003e \u0027!!omap\\n\u0027 + Array.from({length: n}, (_, i) =\u003e `- k${i}: ${i}`).join(\u0027\\n\u0027) + \u0027\\n\u0027;\n\nfor (const n of [10000, 20000, 40000, 80000]) {\n const d = doc(n), t = Date.now();\n yaml.load(d); // default schema, no options\n console.log(`n=${n} bytes=${d.length} load=${Date.now() - t}ms`);\n}\n```\n\n### Measured (node v20.20.2, default heap, no flags)\n\n**js-yaml 4.3.0**\n\n```\nn=10000 bytes=137787 load=54ms\nn=20000 bytes=297787 load=169ms\nn=40000 bytes=617787 load=646ms\nn=80000 bytes=1257787 load=2607ms\n```\n\n**js-yaml 3.15.0**\n\n```\nn=10000 bytes=137787 load=53ms\nn=20000 bytes=297787 load=166ms\nn=40000 bytes=617787 load=641ms\nn=80000 bytes=1257787 load=2567ms\n```\n\nRuntime grows by a factor of ~4 for each doubling of `n`, which is the\nsignature of O(n\u00b2) (linear growth would be ~2\u00d7).\n\nScaling further: a **2.48 MB** document with 150,000 entries blocked\n`yaml.load()` for **10.8 seconds**.\n\n## Impact\n\nAny service that parses attacker-influenced YAML with js-yaml 3.x or 4.x can be\nstalled with a small input. Because the loop is synchronous, a single request\nblocks the Node.js event loop and stalls every other request in the process \u2014\nso the amplification is per-process, not just per-request.\n\nSuggested severity: consistent with **CVE-2026-59870** (the same weakness in\n5.x), i.e. Availability-only impact, network attack vector, no privileges or\nuser interaction required.\n\n## Suggested fix\n\nMirror the 5.x fix \u2014 replace the linear scan with a `Set`:\n\n```js\n// lib/type/omap.js\nconst seen = new Set()\n// ...\nif (seen.has(pairKey)) return false\nseen.add(pairKey)\n```\n\nThis preserves the existing duplicate-key rejection semantics exactly while\nmaking resolution O(n). A `maxOmapLength`-style cap would also work, but the\n`Set` matches what 5.x already ships and requires no new option.\n\n## References\n\n- CVE-2026-59870 / GHSA-724g-mxrg-4qvm \u2014 same weakness in 5.0.0\u20135.2.0, fixed in 5.2.1\n- `lib/type/omap.js` (3.x, 4.x) \u2014 the affected resolver\n- `lib/schema/default.js` \u2014 registers `!!omap` in the default schema\n\n## Discovery\n\nFound by an automated static-analysis and executed-proof-of-concept scanner run\nagainst js-yaml 4.2.0, then manually verified against 3.15.0 and 4.3.0 by\nexecuting the proof of concept above. All timings in this report were measured\non the **current** releases of each line, not on the version originally scanned.",
"id": "GHSA-5p4m-2wfm-xmqj",
"modified": "2026-08-06T20:27:32Z",
"published": "2026-08-06T20:27:32Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nodeca/js-yaml/security/advisories/GHSA-5p4m-2wfm-xmqj"
},
{
"type": "PACKAGE",
"url": "https://github.com/nodeca/js-yaml"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "JS-YAML: Quadratic CPU consumption in !!omap resolution (3.x and 4.x) \u2014 CVE-2026-59870 fix not backported"
}
GHSA-7RQJ-J65F-68WH
Vulnerability from github – Published: 2026-07-23 14:40 – Updated: 2026-08-12 20:27Summary
The default email-address normalizer used by the email/magic-link sign-in flow validates the address before applying Unicode normalization. An address can contain a Unicode character that is not an ASCII @ (U+0040) but canonicalizes to one under NFKC/NFKD normalization (the normalization commonly applied by mail libraries and services for internationalized email). Such an address passes the normalizer's single-@ check, but a downstream mail library that normalizes the string then sees two @ separators and may deliver the passwordless sign-in link to a different recipient than intended. This is an instance of validating before canonicalizing.
Am I affected?
You may be affected if all of the following hold:
- You use
next-auth>= 4.0.0, < 4.24.14, or@auth/core>= 0.1.0, < 0.41.3. - You have the email / magic-link (passwordless) provider enabled.
- You rely on the built-in default identifier normalizer (you have not supplied your own
normalizeIdentifier). - Your
sendVerificationRequestimplementation uses a mail library or delivery service that applies Unicode normalization to recipient addresses (most internationalized-email/SMTPUTF8-capable senders do).
You are not affected if you do not use the email provider, or if your normalizer/mailer rejects or canonicalizes non-ASCII addresses before they are validated.
Impact
- Account takeover: an attacker who knows a victim's email address can request a magic link that is delivered to an attacker-controlled mailbox, then use it to sign in as the victim.
- No victim interaction is required to misroute the link; the attacker initiates the flow.
Patched version
The fix applies Unicode (NFKC) normalization before the address is validated, so homoglyph separators are collapsed and rejected up front. Upgrade to the first release containing this fix (pending; this advisory will be updated with the exact patched version before publication). No application code changes are required after upgrading.
Workarounds
If you cannot upgrade immediately:
- Supply a custom
normalizeIdentifieron the email provider that callsidentifier.normalize("NFKC")(and lower-cases/trims) before any validation, and rejects addresses that do not contain exactly one@after normalization. - Or reject any address whose local part or domain contains non-ASCII characters, if your user base does not require internationalized email addresses.
Credit
Reported by @kakashi-kx. Thank you for the responsible disclosure.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@auth/core"
},
"ranges": [
{
"events": [
{
"introduced": "0.1.0"
},
{
"fixed": "0.41.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "next-auth"
},
"ranges": [
{
"events": [
{
"introduced": "4.10.3"
},
{
"fixed": "4.24.15"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.0.0-beta.31"
},
"package": {
"ecosystem": "npm",
"name": "next-auth"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0-beta.1"
},
{
"fixed": "5.0.0-beta.32"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-73420"
],
"database_specific": {
"cwe_ids": [
"CWE-180"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-23T14:40:15Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "## Summary\n\nThe default email-address normalizer used by the email/magic-link sign-in flow validates the address **before** applying Unicode normalization. An address can contain a Unicode character that is not an ASCII `@` (U+0040) but canonicalizes to one under NFKC/NFKD normalization (the normalization commonly applied by mail libraries and services for internationalized email). Such an address passes the normalizer\u0027s single-`@` check, but a downstream mail library that normalizes the string then sees two `@` separators and may deliver the passwordless sign-in link to a different recipient than intended. This is an instance of validating before canonicalizing.\n\n## Am I affected?\n\nYou may be affected if **all** of the following hold:\n\n- You use `next-auth` `\u003e= 4.0.0, \u003c 4.24.14`, or `@auth/core` `\u003e= 0.1.0, \u003c 0.41.3`.\n- You have the email / magic-link (passwordless) provider enabled.\n- You rely on the built-in default identifier normalizer (you have not supplied your own `normalizeIdentifier`).\n- Your `sendVerificationRequest` implementation uses a mail library or delivery service that applies Unicode normalization to recipient addresses (most internationalized-email/SMTPUTF8-capable senders do).\n\nYou are **not** affected if you do not use the email provider, or if your normalizer/mailer rejects or canonicalizes non-ASCII addresses before they are validated.\n\n## Impact\n\n- Account takeover: an attacker who knows a victim\u0027s email address can request a magic link that is delivered to an attacker-controlled mailbox, then use it to sign in as the victim.\n- No victim interaction is required to misroute the link; the attacker initiates the flow.\n\n## Patched version\n\nThe fix applies Unicode (NFKC) normalization before the address is validated, so homoglyph separators are collapsed and rejected up front. Upgrade to the first release containing this fix (pending; this advisory will be updated with the exact patched version before publication). No application code changes are required after upgrading.\n\n## Workarounds\n\nIf you cannot upgrade immediately:\n\n- Supply a custom `normalizeIdentifier` on the email provider that calls `identifier.normalize(\"NFKC\")` (and lower-cases/trims) **before** any validation, and rejects addresses that do not contain exactly one `@` after normalization.\n- Or reject any address whose local part or domain contains non-ASCII characters, if your user base does not require internationalized email addresses.\n\n## Credit\n\nReported by @kakashi-kx. Thank you for the responsible disclosure.",
"id": "GHSA-7rqj-j65f-68wh",
"modified": "2026-08-12T20:27:03Z",
"published": "2026-07-23T14:40:15Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/security/advisories/GHSA-7rqj-j65f-68wh"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/commit/19d2feb24359fa8c79418907fc68d9ec8152ca94"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/commit/a63eee12a1a20cb35209e44195b097868517b9a0"
},
{
"type": "PACKAGE",
"url": "https://github.com/nextauthjs/next-auth"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/@auth/core@0.41.3"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/next-auth@4.24.15"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/next-auth@5.0.0-beta.32"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Auth.js: Email normalizer validates the address before Unicode normalization, allowing a homoglyph @ bypass"
}
GHSA-C2J3-45GR-MQC4
Vulnerability from github – Published: 2026-07-21 19:41 – Updated: 2026-07-21 19:41Summary
There is a possible hook-policy inconsistency in DOMPurify 3.4.11 involving CUSTOM_ELEMENT_HANDLING.
When a custom element is allowed via CUSTOM_ELEMENT_HANDLING.tagNameCheck, it appears that the element does not go through afterSanitizeElements in the same way as a normal element. As a result, an application that relies on afterSanitizeElements as a security policy layer to strip sensitive attributes from all elements may see those attributes removed from normal elements but preserved on allowed custom elements.
This does not appear to be a direct DOMPurify XSS or a case where DOMPurify directly allows executable payloads. The preserved value is still inert at sanitize time. The issue becomes relevant when the allowed custom element later re-injects that attribute value into an HTML sink such as innerHTML, creating a second-order XSS gadget.
Details
The issue appears to originate from the control flow in src/purify.ts: line 1672~1691
const _sanitizeDisallowedNode = function (
currentNode: any,
tagName: string
): boolean {
/* Check if we have a custom element to handle */
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
if (
CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
) {
return false;
}
if (
CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
) {
return false;
}
}
CUSTOM_ELEMENT_HANDLING is parsed from user configuration at src/purify.ts: line 741~748
const customElementHandling =
objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') &&
cfg.CUSTOM_ELEMENT_HANDLING &&
typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object'
? clone(cfg.CUSTOM_ELEMENT_HANDLING)
: create(null);
CUSTOM_ELEMENT_HANDLING = create(null);
In particular, tagNameCheck, attributeNameCheck, and allowCustomizedBuiltInElements are copied into the internal CUSTOM_ELEMENT_HANDLING object there.
During element sanitization, _sanitizeElements() checks whether a node is forbidden or not allowlisted at src/purify.ts: line 1805~1814
/* Remove element if anything forbids its presence */
if (
FORBID_TAGS[tagName] ||
(!(
EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&
EXTRA_ELEMENT_HANDLING.tagCheck(tagName)
) &&
!ALLOWED_TAGS[tagName])
) {
return _sanitizeDisallowedNode(currentNode, tagName);
}
If so, it immediately delegates to _sanitizeDisallowedNode(currentNode, tagName) and returns its boolean result.
Inside _sanitizeDisallowedNode(), the custom-element-specific allow path is implemented at src/purify.ts: line 1672~1692
const _sanitizeDisallowedNode = function (
currentNode: any,
tagName: string
): boolean {
/* Check if we have a custom element to handle */
if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {
if (
CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&
regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)
) {
return false;
}
if (
CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&
CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)
) {
return false;
}
}
If the node is treated as a basic custom element and CUSTOM_ELEMENT_HANDLING.tagNameCheck matches, the function returns false immediately at line 1682 or 1689, meaning “do not remove this node”.
That early return false is significant because control returns directly to _sanitizeElements() via the return _sanitizeDisallowedNode(...) at line 1813. As a result, the later logic in _sanitizeElements() is skipped for that custom element instance, including:
- the namespace validation at
src/purify.ts: line 1816~1826
* Check whether element has a valid namespace.
Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype
nodeType getter rather than `instanceof Element`, which is realm-
bound and short-circuits to false for any node minted in a different
realm — letting a foreign-realm element with a forbidden namespace
slip past the namespace check entirely. */
const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;
if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {
_forceRemove(currentNode);
return true;
}
- the fallback-tag mXSS check at
src/purify.ts: line 1828~1837
/* Make sure that older browsers don't get fallback-tag mXSS */
if (
(tagName === 'noscript' ||
tagName === 'noembed' ||
tagName === 'noframes') &&
regExpTest(EXPRESSIONS.FALLBACK_TAG_CLOSE, currentNode.innerHTML)
) {
_forceRemove(currentNode);
return true;
}
- most importantly for this report, the
afterSanitizeElementshook dispatch atsrc/purify.ts: line 1850~1851.
/* Execute a hook if present */
_executeHooks(hooks.afterSanitizeElements, currentNode, null);
In other words, a normal allowlisted element continues through _sanitizeElements() and reaches hooks.afterSanitizeElements, but a disallowed-by-default element that is revived by the CUSTOM_ELEMENT_HANDLING.tagNameCheck path does not. This creates a policy inconsistency: an application that relies on afterSanitizeElements to remove an attribute from all elements will observe that the policy is applied to normal elements but not to custom elements allowed through CUSTOM_ELEMENT_HANDLING.
In the PoC, the application hook removes data-bio from ordinary elements, but the same attribute remains on <x-bio> because the custom-element keep path bypasses afterSanitizeElements. The attribute itself is inert at sanitize time and DOMPurify is not directly allowing executable SVG/HTML through. The security impact appears when the application-defined custom element later reads the preserved data-bio value in connectedCallback() and writes it to innerHTML, turning the preserved attribute into a second-order XSS gadget.
PoC
Reproduced on DOMPurify 3.4.11.
Steps
- Save the following HTML to a file, for example
poc.html. - Open it in a browser.
- Observe that the
divcontrol losesdata-bio, while the allowed custom element keeps it. - Observe that after
connectedCallback()runs, the candidate payload is reinserted into the DOM and executes through the custom element’s own sink.
HTML PoC
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.4.11/purify.min.js"></script>
</head>
<body>
<pre id="result"></pre>
<script>
window.__controlFired = false;
window.__candidateFired = false;
customElements.define("x-bio", class extends HTMLElement {
connectedCallback() {
const bio = this.getAttribute("data-bio");
if (bio) this.innerHTML = bio;
}
});
DOMPurify.addHook("afterSanitizeElements", node => {
if (node.hasAttribute && node.hasAttribute("data-bio")) {
node.removeAttribute("data-bio");
}
});
const config = {
CUSTOM_ELEMENT_HANDLING: {
tagNameCheck: /^x-/
}
};
const controlInput =
'<div data-bio="<img src=x onerror=window.__controlFired=true>"></div>';
const candidateInput =
'<x-bio data-bio="<img src=x onerror=window.__candidateFired=true>"></x-bio>';
const cleanControl = DOMPurify.sanitize(controlInput, config);
const cleanCandidate = DOMPurify.sanitize(candidateInput, config);
const container = document.createElement("div");
container.innerHTML = cleanCandidate;
document.body.appendChild(container);
setTimeout(() => {
document.getElementById("result").textContent =
"This is not direct DOMPurify XSS.\n" +
"The payload becomes executable only after x-bio writes data-bio into innerHTML.\n\n" +
"control: " + cleanControl + "\n" +
"candidate: " + cleanCandidate + "\n" +
"after connectedCallback: " + container.innerHTML + "\n" +
"control fired: " + window.__controlFired + "\n" +
"candidate fired: " + window.__candidateFired;
}, 100);
</script>
</body>
</html>
Expected result
control: <div></div>
candidate: <x-bio data-bio="<img src=x onerror=window.__candidateFired=true>"></x-bio>
after connectedCallback: <x-bio data-bio="..."><img src="x" onerror="window.__candidateFired=true"></x-bio>
control fired: false
candidate fired: true
This is output of HTML PoC.
Impact
This does not appear to affect DOMPurify’s default configuration as a direct sanitizer bypass.
The impact is limited to applications that:
- enable
CUSTOM_ELEMENT_HANDLING, - rely on
afterSanitizeElementsas a security policy layer, - expect that hook to apply uniformly to all surviving elements,
- and have allowed custom elements that later re-inject preserved attribute values into
innerHTMLor another HTML sink.
In that situation, the behavior can become a second-order XSS gadget because a security-relevant attribute is removed from normal elements but remains on allowed custom elements.
Possible fixes or mitigations might include
- ensuring that allowed custom elements also consistently pass through
afterSanitizeElements - documenting clearly that elements preserved via
CUSTOM_ELEMENT_HANDLINGmay not participate in the same post-element hook flow as normal allowlisted elements.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.4.11"
},
"package": {
"ecosystem": "npm",
"name": "dompurify"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.4.12"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-184",
"CWE-693",
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T19:41:07Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "## Summary\n\nThere is a possible hook-policy inconsistency in DOMPurify 3.4.11 involving `CUSTOM_ELEMENT_HANDLING`.\n\nWhen a custom element is allowed via `CUSTOM_ELEMENT_HANDLING.tagNameCheck`, it appears that the element does not go through `afterSanitizeElements` in the same way as a normal element. As a result, an application that relies on `afterSanitizeElements` as a security policy layer to strip sensitive attributes from all elements may see those attributes removed from normal elements but preserved on allowed custom elements.\n\nThis does not appear to be a direct DOMPurify XSS or a case where DOMPurify directly allows executable payloads. The preserved value is still inert at sanitize time. The issue becomes relevant when the allowed custom element later re-injects that attribute value into an HTML sink such as `innerHTML`, creating a second-order XSS gadget.\n\n## Details\n\nThe issue appears to originate from the control flow in `src/purify.ts`: line 1672~1691\n\n```tsx\nconst _sanitizeDisallowedNode = function (\n currentNode: any,\n tagName: string\n ): boolean {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] \u0026\u0026 _isBasicCustomElement(tagName)) {\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp \u0026\u0026\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n ) {\n return false;\n }\n\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function \u0026\u0026\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n ) {\n return false;\n }\n }\n```\n\n`CUSTOM_ELEMENT_HANDLING` is parsed from user configuration at `src/purify.ts`: line 741~748\n\n```tsx\nconst customElementHandling =\n objectHasOwnProperty(cfg, \u0027CUSTOM_ELEMENT_HANDLING\u0027) \u0026\u0026\n cfg.CUSTOM_ELEMENT_HANDLING \u0026\u0026\n typeof cfg.CUSTOM_ELEMENT_HANDLING === \u0027object\u0027\n ? clone(cfg.CUSTOM_ELEMENT_HANDLING)\n : create(null);\n\n CUSTOM_ELEMENT_HANDLING = create(null);\n```\n\nIn particular, `tagNameCheck`, `attributeNameCheck`, and `allowCustomizedBuiltInElements` are copied into the internal `CUSTOM_ELEMENT_HANDLING` object there.\n\nDuring element sanitization, `_sanitizeElements()` checks whether a node is forbidden or not allowlisted at `src/purify.ts`: line 1805~1814\n\n```tsx\n/* Remove element if anything forbids its presence */\n if (\n FORBID_TAGS[tagName] ||\n (!(\n EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function \u0026\u0026\n EXTRA_ELEMENT_HANDLING.tagCheck(tagName)\n ) \u0026\u0026\n !ALLOWED_TAGS[tagName])\n ) {\n return _sanitizeDisallowedNode(currentNode, tagName);\n }\n```\n\nIf so, it immediately delegates to `_sanitizeDisallowedNode(currentNode, tagName)` and returns its boolean result.\n\nInside `_sanitizeDisallowedNode()`, the custom-element-specific allow path is implemented at `src/purify.ts`: line 1672~1692\n\n```tsx\nconst _sanitizeDisallowedNode = function (\n currentNode: any,\n tagName: string\n ): boolean {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] \u0026\u0026 _isBasicCustomElement(tagName)) {\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp \u0026\u0026\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n ) {\n return false;\n }\n\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function \u0026\u0026\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n ) {\n return false;\n }\n }\n```\n\nIf the node is treated as a basic custom element and `CUSTOM_ELEMENT_HANDLING.tagNameCheck` matches, the function returns `false` immediately at line 1682 or 1689, meaning \u201cdo not remove this node\u201d.\n\nThat early `return false` is significant because control returns directly to `_sanitizeElements()` via the `return _sanitizeDisallowedNode(...)` at line 1813. As a result, the later logic in `_sanitizeElements()` is skipped for that custom element instance, including:\n\n- the namespace validation at `src/purify.ts`: line 1816~1826\n\n```tsx\n* Check whether element has a valid namespace.\n Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype\n nodeType getter rather than `instanceof Element`, which is realm-\n bound and short-circuits to false for any node minted in a different\n realm \u2014 letting a foreign-realm element with a forbidden namespace\n slip past the namespace check entirely. */\n const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;\n if (nt === NODE_TYPE.element \u0026\u0026 !_checkValidNamespace(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n```\n\n- the fallback-tag mXSS check at `src/purify.ts`: line 1828~1837\n\n```tsx\n/* Make sure that older browsers don\u0027t get fallback-tag mXSS */\n if (\n (tagName === \u0027noscript\u0027 ||\n tagName === \u0027noembed\u0027 ||\n tagName === \u0027noframes\u0027) \u0026\u0026\n regExpTest(EXPRESSIONS.FALLBACK_TAG_CLOSE, currentNode.innerHTML)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n```\n\n- most importantly for this report, the `afterSanitizeElements` hook dispatch at `src/purify.ts`: line 1850~1851.\n\n```tsx\n /* Execute a hook if present */\n _executeHooks(hooks.afterSanitizeElements, currentNode, null);\n```\n\nIn other words, a normal allowlisted element continues through `_sanitizeElements()` and reaches `hooks.afterSanitizeElements`, but a disallowed-by-default element that is revived by the `CUSTOM_ELEMENT_HANDLING.tagNameCheck` path does not. This creates a policy inconsistency: an application that relies on `afterSanitizeElements` to remove an attribute from all elements will observe that the policy is applied to normal elements but not to custom elements allowed through `CUSTOM_ELEMENT_HANDLING`.\n\nIn the PoC, the application hook removes `data-bio` from ordinary elements, but the same attribute remains on `\u003cx-bio\u003e` because the custom-element keep path bypasses `afterSanitizeElements`. The attribute itself is inert at sanitize time and DOMPurify is not directly allowing executable SVG/HTML through. The security impact appears when the application-defined custom element later reads the preserved `data-bio` value in `connectedCallback()` and writes it to `innerHTML`, turning the preserved attribute into a second-order XSS gadget.\n\n## PoC\n\nReproduced on DOMPurify 3.4.11.\n\n### Steps\n\n1. Save the following HTML to a file, for example `poc.html`.\n2. Open it in a browser.\n3. Observe that the `div` control loses `data-bio`, while the allowed custom element keeps it.\n4. Observe that after `connectedCallback()` runs, the candidate payload is reinserted into the DOM and executes through the custom element\u2019s own sink.\n\n### HTML PoC\n\n```html\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n\u003chead\u003e\n \u003cmeta charset=\"UTF-8\"\u003e\n \u003cscript src=\"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.4.11/purify.min.js\"\u003e\u003c/script\u003e\n\u003c/head\u003e\n\u003cbody\u003e\n\u003cpre id=\"result\"\u003e\u003c/pre\u003e\n\n\u003cscript\u003e\nwindow.__controlFired = false;\nwindow.__candidateFired = false;\n\ncustomElements.define(\"x-bio\", class extends HTMLElement {\n connectedCallback() {\n const bio = this.getAttribute(\"data-bio\");\n if (bio) this.innerHTML = bio;\n }\n});\n\nDOMPurify.addHook(\"afterSanitizeElements\", node =\u003e {\n if (node.hasAttribute \u0026\u0026 node.hasAttribute(\"data-bio\")) {\n node.removeAttribute(\"data-bio\");\n }\n});\n\nconst config = {\n CUSTOM_ELEMENT_HANDLING: {\n tagNameCheck: /^x-/\n }\n};\n\nconst controlInput =\n \u0027\u003cdiv data-bio=\"\u0026lt;img src=x onerror=window.__controlFired=true\u0026gt;\"\u003e\u003c/div\u003e\u0027;\n\nconst candidateInput =\n \u0027\u003cx-bio data-bio=\"\u0026lt;img src=x onerror=window.__candidateFired=true\u0026gt;\"\u003e\u003c/x-bio\u003e\u0027;\n\nconst cleanControl = DOMPurify.sanitize(controlInput, config);\nconst cleanCandidate = DOMPurify.sanitize(candidateInput, config);\n\nconst container = document.createElement(\"div\");\ncontainer.innerHTML = cleanCandidate;\ndocument.body.appendChild(container);\n\nsetTimeout(() =\u003e {\n document.getElementById(\"result\").textContent =\n \"This is not direct DOMPurify XSS.\\n\" +\n \"The payload becomes executable only after x-bio writes data-bio into innerHTML.\\n\\n\" +\n \"control: \" + cleanControl + \"\\n\" +\n \"candidate: \" + cleanCandidate + \"\\n\" +\n \"after connectedCallback: \" + container.innerHTML + \"\\n\" +\n \"control fired: \" + window.__controlFired + \"\\n\" +\n \"candidate fired: \" + window.__candidateFired;\n}, 100);\n\u003c/script\u003e\n\u003c/body\u003e\n\u003c/html\u003e\n```\n\n### Expected result\n\n```\ncontrol: \u003cdiv\u003e\u003c/div\u003e\ncandidate: \u003cx-bio data-bio=\"\u003cimg src=x onerror=window.__candidateFired=true\u003e\"\u003e\u003c/x-bio\u003e\nafter connectedCallback: \u003cx-bio data-bio=\"...\"\u003e\u003cimg src=\"x\" onerror=\"window.__candidateFired=true\"\u003e\u003c/x-bio\u003e\ncontrol fired: false\ncandidate fired: true\n```\n\nThis is output of HTML PoC.\n\n\u003cimg width=\"1917\" height=\"961\" alt=\"poc\" src=\"https://github.com/user-attachments/assets/80e22989-5779-42f8-8ffb-106e9a4c2b10\" /\u003e\n\n\n## Impact\n\nThis does not appear to affect DOMPurify\u2019s default configuration as a direct sanitizer bypass.\n\nThe impact is limited to applications that:\n\n- enable `CUSTOM_ELEMENT_HANDLING`,\n- rely on `afterSanitizeElements` as a security policy layer,\n- expect that hook to apply uniformly to all surviving elements,\n- and have allowed custom elements that later re-inject preserved attribute values into `innerHTML` or another HTML sink.\n\nIn that situation, the behavior can become a second-order XSS gadget because a security-relevant attribute is removed from normal elements but remains on allowed custom elements.\n\nPossible fixes or mitigations might include\n\n- ensuring that allowed custom elements also consistently pass through `afterSanitizeElements`\n- documenting clearly that elements preserved via `CUSTOM_ELEMENT_HANDLING` may not participate in the same post-element hook flow as normal allowlisted elements.",
"id": "GHSA-c2j3-45gr-mqc4",
"modified": "2026-07-21T19:41:07Z",
"published": "2026-07-21T19:41:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4"
},
{
"type": "WEB",
"url": "https://github.com/cure53/DOMPurify/pull/1537"
},
{
"type": "WEB",
"url": "https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197"
},
{
"type": "PACKAGE",
"url": "https://github.com/cure53/DOMPurify"
},
{
"type": "WEB",
"url": "https://github.com/cure53/DOMPurify/releases/tag/3.4.12"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N",
"type": "CVSS_V4"
}
],
"summary": "DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements."
}
GHSA-F88M-G3JW-G9CJ
Vulnerability from github – Published: 2026-07-21 22:07 – Updated: 2026-07-21 22:07Impact
A number of vulnerabilities, two rated as "High" severity using CVSSv4, have been discovered and fixed in the upstream libvips dependency.
Those processing untrusted input with versions of sharp prior to 0.35.0 are affected.
Patches
Using prebuilt binaries provided by sharp?
Most people rely on the prebuilt binaries provided by sharp.
Please upgrade sharp to the latest version, currently 0.35.3, which provides libvips 8.18.3.
Using a globally-installed libvips?
Please ensure you are using the latest libvips 8.18.3.
Workarounds
Add the following to your code to prevent sharp from decoding GIF, TIFF and VIPS images.
sharp.block({ operation: ["VipsForeignLoadNsgif", "VipsForeignLoadTiff", "VipsForeignLoadVips"] });
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "sharp"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.35.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-1395"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T22:07:17Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Impact\n\nA number of vulnerabilities, two rated as \"High\" severity using CVSSv4, have been discovered and fixed in the upstream libvips dependency.\n\nThose processing untrusted input with versions of sharp prior to 0.35.0 are affected.\n\n### Patches\n\n#### Using prebuilt binaries provided by sharp?\n\nMost people rely on the prebuilt binaries provided by sharp.\n\nPlease upgrade sharp to the latest version, currently 0.35.3, which provides libvips 8.18.3.\n\n#### Using a globally-installed libvips?\n\nPlease ensure you are using the latest libvips 8.18.3.\n\n### Workarounds\nAdd the following to your code to prevent sharp from decoding GIF, TIFF and VIPS images.\n```js\nsharp.block({ operation: [\"VipsForeignLoadNsgif\", \"VipsForeignLoadTiff\", \"VipsForeignLoadVips\"] });\n```",
"id": "GHSA-f88m-g3jw-g9cj",
"modified": "2026-07-21T22:07:17Z",
"published": "2026-07-21T22:07:17Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/libvips/libvips/security/advisories/GHSA-2fcj-gj27-279x"
},
{
"type": "WEB",
"url": "https://github.com/libvips/libvips/security/advisories/GHSA-523x-vhfw-6r76"
},
{
"type": "WEB",
"url": "https://github.com/libvips/libvips/security/advisories/GHSA-jmwm-wc68-mhwm"
},
{
"type": "WEB",
"url": "https://github.com/libvips/libvips/security/advisories/GHSA-r98w-4fp7-m9c7"
},
{
"type": "WEB",
"url": "https://github.com/lovell/sharp/security/advisories/GHSA-f88m-g3jw-g9cj"
},
{
"type": "PACKAGE",
"url": "https://github.com/lovell/sharp"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "sharp inherited vulnerabilities in libvips: CVE-2026-33327, CVE-2026-33328, CVE-2026-35590, CVE-2026-35591"
}
GHSA-FRVP-7C67-39W9
Vulnerability from github – Published: 2026-07-21 18:17 – Updated: 2026-08-12 18:26The same as the hono core Path traversal in serve-static on Windows via encoded backslash (%5C).
Summary
On Windows hosts, an encoded backslash (%5C) in the request path decodes to \, which the Windows path resolver treats as a separator. serve-static then resolves a single URL segment such as admin\secret.txt into a nested file under the root and serves it, letting an attacker read static files meant to be protected behind prefix-mounted middleware. Directory escape (..) remains blocked.
Details
The router splits paths only on /, so /admin%5Csecret.txt is one segment and middleware on /admin/* does not run. The serve-static guard rejects ./.. and consecutive separators but lets a lone \ through; on Windows the file resolver re-splits it into the protected subtree.
This affects Windows hosts serving static files via the Node, Bun, or Deno adapters that guard a static subtree with prefix-mounted middleware.
Impact
An unauthenticated attacker can read static files under a middleware-guarded prefix on Windows hosts. The read stays within the configured root; escape outside the root is not possible.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@hono/node-server"
},
"ranges": [
{
"events": [
{
"introduced": "2.0.0"
},
{
"fixed": "2.0.5"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@hono/node-server"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.19.15"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-22"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T18:17:25Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "The same as the `hono` core [Path traversal in `serve-static` on Windows via encoded backslash (`%5C`)](https://github.com/honojs/hono/security/advisories/GHSA-wwfh-h76j-fc44).\n\n### Summary\n\nOn Windows hosts, an encoded backslash (`%5C`) in the request path decodes to `\\`, which the Windows path resolver treats as a separator. `serve-static` then resolves a single URL segment such as `admin\\secret.txt` into a nested file under the root and serves it, letting an attacker read static files meant to be protected behind prefix-mounted middleware. Directory escape (`..`) remains blocked.\n\n### Details\n\nThe router splits paths only on `/`, so `/admin%5Csecret.txt` is one segment and middleware on `/admin/*` does not run. The `serve-static` guard rejects `.`/`..` and consecutive separators but lets a lone `\\` through; on Windows the file resolver re-splits it into the protected subtree.\n\nThis affects Windows hosts serving static files via the Node, Bun, or Deno adapters that guard a static subtree with prefix-mounted middleware.\n\n### Impact\n\nAn unauthenticated attacker can read static files under a middleware-guarded prefix on Windows hosts. The read stays within the configured root; escape outside the root is not possible.",
"id": "GHSA-frvp-7c67-39w9",
"modified": "2026-08-12T18:26:49Z",
"published": "2026-07-21T18:17:25Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/honojs/node-server/security/advisories/GHSA-frvp-7c67-39w9"
},
{
"type": "WEB",
"url": "https://github.com/honojs/node-server/commit/cd076e117cfe4cb8d31f9eb11d2e8f38a6cb8faf"
},
{
"type": "PACKAGE",
"url": "https://github.com/honojs/node-server"
},
{
"type": "WEB",
"url": "https://github.com/honojs/node-server/releases/tag/v2.0.5"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Node.js Adapter for Hono: Path traversal in `serve-static` on Windows via encoded backslash (`%5C`)"
}
GHSA-P63J-VCC4-9VMV
Vulnerability from github – Published: 2026-07-21 19:36 – Updated: 2026-08-13 18:19Summary
Browser Mode exposes a set of built-in "commands" that run on the Node.js side of the test runner and can touch the local filesystem (taking screenshots, managing Playwright traces, uploading files for <input type="file">, comparing screenshots).
Several of these commands accept a file path from the browser and act on it without checking the allowWrite permission gate and without confining the path to the project directory. A client that can reach the Browser Mode API can therefore read, create, overwrite, or delete files anywhere the Vitest process can access, even when allowWrite is false.
This matters most when the Browser Mode API is exposed to the network (for example test.api.host is set, or the dev server is reachable from another machine or origin). In that configuration allowWrite defaults to false precisely to block file access, and these commands bypass that protection. On a default localhost-only setup with trusted test code, there is no untrusted party in a position to exploit it. The gap still matters wherever you rely on allowWrite: false to contain untrusted test code, because these commands ignore that flag.
Affected commands and impact
| Command | Operation | Impact |
|---|---|---|
upload (Playwright + WebdriverIO) |
Read | Arbitrary local file read; contents are loaded into the page and readable by test code. Highest-impact case. |
takeScreenshot (Playwright + WebdriverIO) |
Write | Writes a PNG to an arbitrary path (absolute path used verbatim), creating parent directories. |
screenshotMatcher |
Write | Writes reference/diff PNGs; directory derived from client path allows partial traversal. |
stopChunkTrace |
Write | Writes a Playwright trace .zip to a path escapable via ../ in the trace name. |
deleteTracing |
Delete | Deletes arbitrary files by path. |
annotateTraces |
Read (disclosure) | Records a client-controlled attachment path that the reporter copies into the attachments directory, disclosing file contents. |
The writes do not let an attacker choose the file contents (they produce PNG images or trace archives), so the integrity impact is creating, overwriting, or deleting a file at an arbitrary path rather than writing a chosen payload. The reads (upload, annotateTraces) are more serious because they expose the full contents of an arbitrary file.
The fix adds, to every file-touching provider command, an allowWrite check for write/delete operations and path confinement to the project root (matching the existing fs command pattern), so client-supplied absolute paths and ../ traversal are rejected.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@vitest/browser"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.0"
},
{
"fixed": "4.1.10"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@vitest/browser"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.2.7"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@vitest/browser"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0-beta.1"
},
{
"fixed": "5.0.0-beta.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-73653"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-552",
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-21T19:36:12Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "## Summary\n\nBrowser Mode exposes a set of built-in \"commands\" that run on the Node.js side of the test runner and can touch the local filesystem (taking screenshots, managing Playwright traces, uploading files for `\u003cinput type=\"file\"\u003e`, comparing screenshots).\n\nSeveral of these commands accept a file path from the browser and act on it without checking the `allowWrite` permission gate and without confining the path to the project directory. A client that can reach the Browser Mode API can therefore read, create, overwrite, or delete files anywhere the Vitest process can access, even when `allowWrite` is `false`.\n\nThis matters most when the Browser Mode API is exposed to the network (for example `test.api.host` is set, or the dev server is reachable from another machine or origin). In that configuration `allowWrite` defaults to `false` precisely to block file access, and these commands bypass that protection. On a default localhost-only setup with trusted test code, there is no untrusted party in a position to exploit it. The gap still matters wherever you rely on `allowWrite: false` to contain untrusted test code, because these commands ignore that flag.\n\n## Affected commands and impact\n\n| Command | Operation | Impact |\n|---|---|---|\n| `upload` (Playwright + WebdriverIO) | Read | Arbitrary local file read; contents are loaded into the page and readable by test code. Highest-impact case. |\n| `takeScreenshot` (Playwright + WebdriverIO) | Write | Writes a PNG to an arbitrary path (absolute path used verbatim), creating parent directories. |\n| `screenshotMatcher` | Write | Writes reference/diff PNGs; directory derived from client path allows partial traversal. |\n| `stopChunkTrace` | Write | Writes a Playwright trace `.zip` to a path escapable via `../` in the trace name. |\n| `deleteTracing` | Delete | Deletes arbitrary files by path. |\n| `annotateTraces` | Read (disclosure) | Records a client-controlled attachment path that the reporter copies into the attachments directory, disclosing file contents. |\n\nThe writes do not let an attacker choose the file contents (they produce PNG images or trace archives), so the integrity impact is creating, overwriting, or deleting a file at an arbitrary path rather than writing a chosen payload. The reads (`upload`, `annotateTraces`) are more serious because they expose the full contents of an arbitrary file.\n\nThe fix adds, to every file-touching provider command, an `allowWrite` check for write/delete operations and path confinement to the project root (matching the existing `fs` command pattern), so client-supplied absolute paths and `../` traversal are rejected.",
"id": "GHSA-p63j-vcc4-9vmv",
"modified": "2026-08-13T18:19:12Z",
"published": "2026-07-21T19:36:12Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/security/advisories/GHSA-p63j-vcc4-9vmv"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/pull/10674"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/pull/10679"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/pull/10680"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/commit/33f96a145ef09ca6a43b4e555eb273e64a87be23"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/commit/5c18dd267ff7f47f24cab2f615a16b37d90feb7f"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/commit/b795e36b34969bec50b47a9f29d26f799a6a04fb"
},
{
"type": "PACKAGE",
"url": "https://github.com/vitest-dev/vitest"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/releases/tag/v3.2.7"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/releases/tag/v4.1.10"
},
{
"type": "WEB",
"url": "https://github.com/vitest-dev/vitest/releases/tag/v5.0.0-beta.6"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "@vitest/browser: Browser Mode provider commands bypass the file-access permission gate"
}
GHSA-X445-F3H2-J279
Vulnerability from github – Published: 2026-07-23 14:31 – Updated: 2026-08-13 19:05Summary
Auth.js stores the OAuth/OIDC anti-CSRF checks (state, nonce, and the PKCE verifier) in global cookies that are not bound to the provider that created them. On callback, a check value minted during a sign-in started with one provider can satisfy the callback for a different provider, because the stored cookie is not verified against the callback provider's identity (provider id, issuer, client id, or redirect URI). In a multi-provider app that allows account linking while logged in, this provider-confusion / mix-up condition can let an attacker link their account at a second provider to a victim's user.
Am I affected?
You may be affected if all of the following hold:
- You use
next-auth<= 4.24.14or>= 5.0.0-beta.1, <= 5.0.0-beta.31, or@auth/core<= 0.41.2. - You configure multiple OAuth/OIDC providers.
- You allow users to link additional providers while logged in.
- At least one configured provider's authorization request is observable by an attacker, and at least one target provider's callback can be satisfied without a PKCE verifier (i.e. it relies only on
stateor only onnonce).
You are not affected if you use a single OAuth provider, do not allow logged-in account linking, or all providers enforce PKCE.
Impact
- Account-linking confusion: an attacker can get their account at a target provider linked to the victim's Auth.js user, granting the attacker persistent sign-in to the victim's account through that linked provider.
- Exploitation requires luring the victim into starting a legitimate same-origin flow; it cannot be performed by cross-site request forgery alone, which reduces practical likelihood.
Patched version
The fix binds the OAuth check cookies to the provider/authorization flow that created them, so a callback cannot consume a check value minted for a different provider. Upgrade to the first releases containing this fix (pending; this advisory will be updated with exact patched versions before publication).
Workarounds
If you cannot upgrade immediately:
- Enable PKCE (
checks: ["pkce"], in addition tostate/nonce) on every provider that supports it; PKCE blocks the practical code-swap variant because the attacker cannot observe the relying party's verifier. - Avoid offering logged-in account linking across multiple providers where one provider is lower-trust or attacker-observable.
- Treat
events.linkAccountas sensitive: add audit logging, user notification, or out-of-band confirmation so that any unexpected link is visible (defense-in-depth, not a root-cause fix).
Credit
Reported by @Nadav0077. Thank you for the responsible disclosure.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.41.2"
},
"package": {
"ecosystem": "npm",
"name": "@auth/core"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.41.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.0.0-beta.31"
},
"package": {
"ecosystem": "npm",
"name": "next-auth"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0-beta.1"
},
{
"fixed": "5.0.0-beta.32"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.24.14"
},
"package": {
"ecosystem": "npm",
"name": "next-auth"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.24.15"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-73419"
],
"database_specific": {
"cwe_ids": [
"CWE-345",
"CWE-346",
"CWE-940"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-23T14:31:51Z",
"nvd_published_at": "2026-08-12T21:17:41Z",
"severity": "MODERATE"
},
"details": "## Summary\n\nAuth.js stores the OAuth/OIDC anti-CSRF checks (`state`, `nonce`, and the PKCE verifier) in global cookies that are not bound to the provider that created them. On callback, a check value minted during a sign-in started with one provider can satisfy the callback for a different provider, because the stored cookie is not verified against the callback provider\u0027s identity (provider id, issuer, client id, or redirect URI). In a multi-provider app that allows account linking while logged in, this provider-confusion / mix-up condition can let an attacker link their account at a second provider to a victim\u0027s user.\n\n## Am I affected?\n\nYou may be affected if **all** of the following hold:\n\n- You use `next-auth` `\u003c= 4.24.14` or `\u003e= 5.0.0-beta.1, \u003c= 5.0.0-beta.31`, or `@auth/core` `\u003c= 0.41.2`.\n- You configure multiple OAuth/OIDC providers.\n- You allow users to link additional providers while logged in.\n- At least one configured provider\u0027s authorization request is observable by an attacker, and at least one target provider\u0027s callback can be satisfied without a PKCE verifier (i.e. it relies only on `state` or only on `nonce`).\n\nYou are **not** affected if you use a single OAuth provider, do not allow logged-in account linking, or all providers enforce PKCE.\n\n## Impact\n\n- Account-linking confusion: an attacker can get their account at a target provider linked to the victim\u0027s Auth.js user, granting the attacker persistent sign-in to the victim\u0027s account through that linked provider.\n- Exploitation requires luring the victim into starting a legitimate same-origin flow; it cannot be performed by cross-site request forgery alone, which reduces practical likelihood.\n\n## Patched version\n\nThe fix binds the OAuth check cookies to the provider/authorization flow that created them, so a callback cannot consume a check value minted for a different provider. Upgrade to the first releases containing this fix (pending; this advisory will be updated with exact patched versions before publication).\n\n## Workarounds\n\nIf you cannot upgrade immediately:\n\n- Enable PKCE (`checks: [\"pkce\"]`, in addition to `state`/`nonce`) on every provider that supports it; PKCE blocks the practical code-swap variant because the attacker cannot observe the relying party\u0027s verifier.\n- Avoid offering logged-in account linking across multiple providers where one provider is lower-trust or attacker-observable.\n- Treat `events.linkAccount` as sensitive: add audit logging, user notification, or out-of-band confirmation so that any unexpected link is visible (defense-in-depth, not a root-cause fix).\n\n## Credit\n\nReported by @Nadav0077. Thank you for the responsible disclosure.",
"id": "GHSA-x445-f3h2-j279",
"modified": "2026-08-13T19:05:32Z",
"published": "2026-07-23T14:31:51Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/security/advisories/GHSA-x445-f3h2-j279"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73419"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/pull/13469"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/commit/5bca2399a79ba8d116ca5179b4b1ebcd152e7f05"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/commit/9f7a97fade9b1319bb9ac19fc9828d62e0a2a852"
},
{
"type": "PACKAGE",
"url": "https://github.com/nextauthjs/next-auth"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/@auth/core@0.41.3"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/next-auth@4.24.15"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/next-auth@5.0.0-beta.32"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Auth.js: OAuth state, nonce, and PKCE check cookies are not bound to the provider that created them"
}
GHSA-XMF8-CVQR-RFGJ
Vulnerability from github – Published: 2026-07-23 14:42 – Updated: 2026-08-13 19:05Summary
The exported getToken() helper (next-auth/jwt and @auth/core/jwt) can throw an uncaught exception when it reads a malformed Authorization: Bearer … header. When no session cookie is present, getToken() URL-decodes the bearer value before validating it, and malformed percent-encoding causes the decode step to throw rather than being treated as an invalid token. Because getToken() is commonly called in API routes, middleware, and other request handlers, a single unauthenticated request can trigger an unhandled exception in code paths that authenticate requests.
Am I affected?
You are affected if all of the following hold:
- You use
next-auth<= 5.0.0-beta.25(or@auth/coreexposing the samegetToken()implementation). - Your application calls
getToken()directly — for example in a Route Handler, middleware, or server-side request handler. - You do not wrap that
getToken()call in your owntry/catch.
You are not affected if you only use the framework's auth() helper and never call getToken() yourself, or if every getToken() call site already has its own exception handling.
Impact
- Denial of service: an unauthenticated request carrying a malformed Bearer authorization header can raise an unhandled exception in any handler that calls
getToken(). - The impact is per-request and limited to availability; it does not expose tokens, sessions, or other data, and does not bypass authentication.
CWE-20: Improper Input Validation.
Patched version
The fix makes getToken() treat a malformed Bearer value as an invalid token and return null, matching how other undecodable tokens are already handled. Upgrade to the first release containing this fix (to be published; this advisory will be updated with the exact patched version before publication) and no code changes are required.
Workarounds
If you cannot upgrade immediately, either:
- Config/code-level: wrap your
getToken()calls so a thrown error is treated as "no token", e.g.
ts
let token = null
try {
token = await getToken({ req, secret })
} catch {
token = null
}
- Or strip/normalize the incoming
Authorizationheader at the edge (proxy, middleware) before it reachesgetToken(), rejecting values whose Bearer portion is not valid percent-encoding.
Credit
Reported by @deprrous. Thank you for the responsible disclosure.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@auth/core"
},
"ranges": [
{
"events": [
{
"introduced": "0.1.0"
},
{
"fixed": "0.41.3"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 5.0.0-beta.31"
},
"package": {
"ecosystem": "npm",
"name": "next-auth"
},
"ranges": [
{
"events": [
{
"introduced": "5.0.0-beta.0"
},
{
"fixed": "5.0.0-beta.32"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.24.14"
},
"package": {
"ecosystem": "npm",
"name": "next-auth"
},
"ranges": [
{
"events": [
{
"introduced": "4.0.6"
},
{
"fixed": "4.24.15"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-73418"
],
"database_specific": {
"cwe_ids": [
"CWE-20"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-23T14:42:55Z",
"nvd_published_at": "2026-08-12T21:17:40Z",
"severity": "HIGH"
},
"details": "## Summary\n\nThe exported `getToken()` helper (`next-auth/jwt` and `@auth/core/jwt`) can throw an uncaught exception when it reads a malformed `Authorization: Bearer \u2026` header. When no session cookie is present, `getToken()` URL-decodes the bearer value before validating it, and malformed percent-encoding causes the decode step to throw rather than being treated as an invalid token. Because `getToken()` is commonly called in API routes, middleware, and other request handlers, a single unauthenticated request can trigger an unhandled exception in code paths that authenticate requests.\n\n## Am I affected?\n\nYou are affected if **all** of the following hold:\n\n- You use `next-auth` `\u003c= 5.0.0-beta.25` (or `@auth/core` exposing the same `getToken()` implementation).\n- Your application calls `getToken()` directly \u2014 for example in a Route Handler, middleware, or server-side request handler.\n- You do not wrap that `getToken()` call in your own `try/catch`.\n\nYou are **not** affected if you only use the framework\u0027s `auth()` helper and never call `getToken()` yourself, or if every `getToken()` call site already has its own exception handling.\n\n## Impact\n\n- Denial of service: an unauthenticated request carrying a malformed Bearer authorization header can raise an unhandled exception in any handler that calls `getToken()`.\n- The impact is per-request and limited to availability; it does not expose tokens, sessions, or other data, and does not bypass authentication.\n\nCWE-20: Improper Input Validation.\n\n## Patched version\n\nThe fix makes `getToken()` treat a malformed Bearer value as an invalid token and return `null`, matching how other undecodable tokens are already handled. Upgrade to the first release containing this fix (to be published; this advisory will be updated with the exact patched version before publication) and no code changes are required.\n\n## Workarounds\n\nIf you cannot upgrade immediately, either:\n\n- **Config/code-level:** wrap your `getToken()` calls so a thrown error is treated as \"no token\", e.g.\n\n ```ts\n let token = null\n try {\n token = await getToken({ req, secret })\n } catch {\n token = null\n }\n ```\n\n- Or strip/normalize the incoming `Authorization` header at the edge (proxy, middleware) before it reaches `getToken()`, rejecting values whose Bearer portion is not valid percent-encoding.\n\n## Credit\n\nReported by @deprrous. Thank you for the responsible disclosure.",
"id": "GHSA-xmf8-cvqr-rfgj",
"modified": "2026-08-13T19:05:18Z",
"published": "2026-07-23T14:42:55Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/security/advisories/GHSA-xmf8-cvqr-rfgj"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73418"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/pull/13467"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/pull/13469"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/commit/5bca2399a79ba8d116ca5179b4b1ebcd152e7f05"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/commit/e707770f00c52b3479e43422b0200b059149ed53"
},
{
"type": "PACKAGE",
"url": "https://github.com/nextauthjs/next-auth"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/@auth/core@0.41.3"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/next-auth@4.24.15"
},
{
"type": "WEB",
"url": "https://github.com/nextauthjs/next-auth/releases/tag/next-auth@5.0.0-beta.32"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
"type": "CVSS_V3"
}
],
"summary": "Auth.js: getToken() throws an uncaught exception on malformed Bearer authorization headers"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.