Skip to content

fix: clear CPython CVEs from customer scan of 0.9.14-rc1 (PER-15358) - #329

Merged
Zivxx merged 3 commits into
mainfrom
july-29-vulnerability-fixes
Jul 30, 2026
Merged

fix: clear CPython CVEs from customer scan of 0.9.14-rc1 (PER-15358)#329
Zivxx merged 3 commits into
mainfrom
july-29-vulnerability-fixes

Conversation

@Zivxx

@Zivxx Zivxx commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What prompted this

A customer ran a CPE-based scan against permitio/pdp-v2:0.9.14-rc1 and returned 9 CVEs, all marked Stop=true. It's four separate problems wearing nine hats.

Worth noting up front: 0.9.14-rc1 is commit 8b9b17f — the "clear remaining CVEs" commit. Three of the nine findings are the exact starlette CVEs waived in that commit. Their scanner doesn't read OpenVEX, so our reasoning was invisible to them. That's a communication gap, not a code gap.

Disposition

CVE Component Status
CVE-2026-6019 CPython 3.10.20 Fixed — base image → 3.13.14
CVE-2026-7210 CPython 3.10.20 Fixed — 3.13.14 + expat 2.8.1
CVE-2023-36632 CPython 3.10.20 Fixed — DISPUTED by PSF; 3.13 is outside its < 3.11.4 range
CVE-2026-15308 CPython 3.10.20 Waived (new) — unfixable in any released Python
CVE-2026-48817 / 54283 / 48710 starlette 0.50.0 Already waived in 8b9b17f
CVE-2025-64702 / 2026-40898 quic-go 0.54.1 Belongs in permit-opa — see below

These were not scanner false positives

The tempting read is "CPE noise." That's wrong, and I checked the shipped image rather than trusting advisories:

  • PSF fixed these only on 3.13/3.14/3.15. Every fix commit is a [3.13]/[3.14]/[3.15] backport; there is no 3.10/3.11/3.12 equivalent.
  • The vulnerable code was physically present in 3.10.20: Morsel.js_output() did the bare .replace('"', r'\"') with no encoding, and pyexpat linked expat 2.7.4, under the 2.8.0 the fix requires.

So an interpreter upgrade was the only fix. Python 3.10 also hits EOL in October 2026, so this was due regardless.

Note 3.12 would have cleared nothing — NVD's ranges are unbounded-below (< 3.13.14), so 3.12.13 still matches. ≥ 3.13.14 is required, which forces Alpine 3.22 → 3.23.

Why CVE-2026-15308 is waived rather than fixed

Patched only in 3.15.0b4, and NVD's range is < 3.15.0no released Python satisfies it; every Python image in the world matches today. It's also genuinely unreachable: nothing imports html.parser, not our code and not any package in the image (verified by scanning every .py under site-packages).

The base image patch version floats deliberately, so when 3.13.15 ships this clears automatically and the waiver can be dropped.

quic-go — needs a separate PR in permit-opa

go version -m /app/bin/opa confirms permit-opa (devel)quic-go v0.54.1. The vulnerable HTTP/3 code is linked: 368 http3 references survive in the stripped binary (http3.Transport, http3.Conn, http3.RoundTripOpt).

Caveat for anyone re-checking: the binary is stripped (-ldflags="-s -w"), so go tool nm returns nothing, and grepping the full import path also finds nothing because pclntab stores the short name http3.. Don't conclude "unreachable" from either.

Mitigating: only http3.Transport is present — no http3.Server, so it's client-side only. Still has a real fix, unlike the starlette/ddtrace waivers.

Action: go get github.com/quic-go/quic-go@v0.59.1 && go mod tidy in permit-opa. It's a transitive dep, so run go mod why -m first. This must merge to permit-opa@main before a PDP rebuild picks it up — same constraint as the oras-go bump in PER-15532.

Deliberate scope calls

  • ruff target-version stays py310 while the runtime moves to 3.13. It's a lint floor, not a runtime constraint. Raising it produces 5 findings unrelated to this work — 3× ASYNC109 wanting timeout params rewritten as asyncio.timeout() context managers in update_subscriber.py and proxy/api.py — which would fail the pre-commit job. Worth a follow-up. mypy's python_version does move to 3.13.
  • Base image patch floats (python:3.13-alpine3.23), matching the previous base and the rebuild-picks-it-up posture. 3.13.14 is documented as the hard floor.

Gap this exposes

The Scout gate reads packages, so it never sees the CPython interpreter the way a CPE scanner does. That blind spot is why four Python CVEs reached a customer while the gate was tight enough to catch a ddtrace baggage DoS. Documented inline in tests.yml; worth a follow-up to add CPE scanning or a base-image freshness check.

Verification

Built the main stage on the new base — the risky part being the .python-rundeps sqlite surgery:

PYTHON 3.13.14      EXPAT expat_2.8.1      IMPORTS OK
SQLITE REMOVED OK   PIP REMOVED OK         ENSUREPIP REMOVED OK
  • All deps install and import on 3.13, incl. opal-client/opal-common 0.9.6 and pydantic 1.10.26.
  • Morsel.js_output now encodes its value — CVE-2026-6019 verifiably gone, not just version-bumped past.
  • horizon.main exits SystemExit(1) on a missing API key on both 3.10 and 3.13 — baseline, not a regression.

Full PDP tester hasn't run against this yet; that's what this PR is for.

🤖 Generated with Claude Code

A customer CPE scan of permitio/pdp-v2:0.9.14-rc1 raised 9 CVEs. Three were
the starlette findings already waived in that very commit (0.9.14-rc1 IS
8b9b17f) - their scanner does not read OpenVEX. Two are quic-go in the OPA
binary and belong in permit-opa. The remaining four are CPython itself.

Fixed by moving the base image python:3.10-alpine3.22 -> python:3.13-alpine3.23:
* CVE-2026-6019  - http.cookies Morsel.js_output escaping (fixed in 3.13.14)
* CVE-2026-7210  - expat hash-flooding entropy (needs 3.13.14 AND libexpat
                   >= 2.8.0; the new base ships expat 2.8.1)
* CVE-2023-36632 - email.utils.parseaddr recursion. DISPUTED by PSF and never
                   fixed, but its CPE range is < 3.11.4, so 3.13 is out of it.

These were NOT scanner false positives. PSF fixed them only on the 3.13/3.14/
3.15 branches - each fix commit is a [3.13]/[3.14]/[3.15] backport and there is
no 3.10/3.11/3.12 equivalent - so the vulnerable code really was present in
3.10.20. Verified in the shipped image: js_output did the bare quote-replace
with no encoding, and pyexpat linked expat 2.7.4. An upgrade was the only fix.
Python 3.10 also reaches EOL in October 2026, so this move was due regardless.

Waived:
* CVE-2026-15308 (html.parser quadratic DoS) - cannot be fixed by anyone today.
  PSF patched it only in 3.15.0b4 and NVD's range is < 3.15.0, so no released
  Python satisfies it. Unreachable here: nothing imports html.parser, not the
  PDP's own code and not any package in the image (verified by scanning every
  .py under site-packages). The base image patch version floats deliberately,
  so 3.13.15 will clear this automatically and the waiver can then be dropped.

Not addressed here (different repo):
* CVE-2025-64702 / CVE-2026-40898 - quic-go v0.54.1 in /app/bin/opa, fixed in
  0.57.0 and 0.59.1. `go version -m` confirms the dep, and 368 http3 references
  survive in the stripped binary (http3.Transport, http3.Conn, http3.RoundTripOpt),
  so the vulnerable QPACK decode path IS linked - though only the client is
  present, there is no http3.Server. Fix is `go get quic-go@v0.59.1` in
  permit-opa and MUST merge to permit-opa@main before a PDP rebuild picks it up.

ruff's target-version is deliberately left at py310 while the runtime moves to
3.13. It is a lint floor, not a runtime constraint; raising it surfaces 5
findings unrelated to this work (3x ASYNC109 wanting `timeout` parameters
rewritten as asyncio.timeout() context managers in horizon/facts/update_subscriber.py
and horizon/proxy/api.py, plus UP041/UP043) that would fail the pre-commit job.
mypy's python_version does move to 3.13 to track the real interpreter.

Also documents a gap in the Scout gate: it reads packages, so it never sees the
CPython interpreter the way a CPE scanner does. That blind spot is why four
Python CVEs reached a customer without failing CI.

Verified by building the main stage on the new base: Python 3.13.14, expat
2.8.1, all deps import (incl. opal-client/opal-common 0.9.6, pydantic 1.10.26),
and the .python-rundeps sqlite surgery still works - sqlite3, pip and ensurepip
all removed. horizon.main exits SystemExit(1) on a missing API key on both 3.10
and 3.13, so that is baseline behaviour and not a 3.13 regression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@linear-code

linear-code Bot commented Jul 29, 2026

Copy link
Copy Markdown

PER-15358

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

🔍 Vulnerabilities of permitio/pdp-v2:next

📦 Image Reference permitio/pdp-v2:next
digestsha256:5da584aba45bfe2873d2c4407ecc37af100219c6a83671233cc65a5f63d7fa09
vulnerabilitiescritical: 0 high: 3 medium: 6 low: 4 unspecified: 4
platformlinux/amd64
size133 MB
packages250
📦 Base Image python:3.13-alpine3.23
also known as
  • 3.13.14-alpine3.23
  • e0e75f8d10947da66ae425727cad4e480c65f32018367f4006f9eba40c48cd5c
digestsha256:72c39ab9dbf2227aa91ec2246e6492260ee2530c36bdf37b208394b42d757b60
vulnerabilitiescritical: 0 high: 2 medium: 1 low: 0
critical: 0 high: 2 medium: 2 low: 1 starlette 0.50.0 (pypi)

pkg:pypi/starlette@0.50.0

high 7.5: CVE--2026--54283 Allocation of Resources Without Limits or Throttling

Affected range>=0.4.1
<1.3.1
Fixed version1.3.1
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.397%
EPSS Percentile32nd percentile
Description

Summary

request.form() accepts max_fields and max_part_size to bound resource consumption while parsing form data. These limits are enforced for multipart/form-data, but silently ignored for application/x-www-form-urlencoded. An unauthenticated attacker can therefore send a urlencoded body with an arbitrarily large number of fields or an arbitrarily large field, even when the application configured limits it believed would apply.

Details

request.form() dispatches to a different parser depending on the Content-Type. For multipart/form-data the max_files, max_fields, and max_part_size limits are forwarded to the parser, but for application/x-www-form-urlencoded the parser is constructed without them. It has no max_fields or max_part_size parameter to receive them, and it appends every field with no count check and accumulates each field's name and value with no size check. The configured limits are therefore both unreachable and unenforced for url-encoded bodies.

Because the url-encoded parser does its work synchronously between stream reads, the two attack shapes have different effects:

  • Field count drives CPU and event-loop blocking. A body of ~1,000,000 fields (a sub-10MB payload such as f0=v&f1=v&...) blocks the worker's event loop for several seconds while parsing, during which the worker serves no other request.
  • Field size drives memory. A single large field value (e.g. a 50MB value) is buffered in full to build the FormData, forcing memory allocation proportional to the request body.

The equivalent multipart/form-data request is correctly rejected with 400 Too many fields / 400 Field exceeded maximum size.

Impact

This Denial of service (DoS) vulnerability affects all applications built with Starlette (or FastAPI) that call request.form() on application/x-www-form-urlencoded requests. A single request with a very large number of fields blocks the event loop for several seconds, and a single request with a very large field forces unbounded memory allocation; in either case, parallel requests can render the service unusable. A reverse proxy that enforces a request body size limit reduces but does not eliminate the exposure, since a sub-10MB body is already enough to block the event loop.

Mitigation

Upgrade to a patched version, which forwards max_fields and max_part_size to the url-encoded parser and enforces them while parsing, raising before the oversized field or excess fields are accumulated. The defaults match multipart/form-data (max_fields=1000, max_part_size=1MB) and can be customized via request.form(max_fields=..., max_part_size=...).

high 7.5: CVE--2026--48818 Server-Side Request Forgery (SSRF)

Affected range<1.1.0
Fixed version1.1.0
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
EPSS Score0.368%
EPSS Percentile29th percentile
Description

Summary

When serving static files on Windows, StaticFiles resolves the requested path with os.path.realpath. If a UNC path (such as \\attacker.com\share) reaches the resolver, realpath causes the process to open a connection to the remote host over SMB (port 445). This is a server-side request forgery (SSRF) that leaks the service account's NTLMv2 credentials to the attacker-controlled host, which can then be cracked offline or relayed to other hosts.

Details

StaticFiles.lookup_path() joins the requested path onto the served directory and calls os.path.realpath on the result before checking containment with os.path.commonpath. On Windows, a UNC path is absolute, so os.path.join discards the served directory and realpath resolves the bare UNC path, triggering the outbound SMB connection and NTLM authentication before the containment check rejects the path. The HTTP response is a benign 404, but the credential disclosure has already happened. POSIX systems are not affected.

This only affects the default configuration (follow_symlink=False), which uses os.path.realpath. The follow_symlink=True branch uses os.path.abspath, which performs no I/O.

Impact

Applications running on Windows that serve files with StaticFiles (directly, or via a framework built on Starlette such as FastAPI) in the default configuration are affected. StaticFiles is typically unauthenticated, so any client can trigger the SMB connection and leak the service account's NTLMv2 hash. A secondary impact is discovering internal hosts reachable over SMB by timing responses for valid versus invalid addresses.

Mitigation

Applications not running on Windows are not affected. On Windows, serving static files through a dedicated web server (such as nginx or IIS) instead of StaticFiles avoids the issue. Blocking outbound SMB (port 445) from the application host prevents the credential disclosure even if a UNC path is resolved.

medium 6.5: CVE--2026--48710 Improper Validation of Unsafe Equivalence in Input

Affected range<=1.0.0
Fixed version1.0.1
CVSS Score6.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
EPSS Score1.839%
EPSS Percentile77th percentile
Description

Summary

In affected versions, the HTTP Host request header was not validated before being used to reconstruct request.url. Because the routing algorithm relies on the raw HTTP path while request.url is rebuilt from the Host header, a malformed header could make request.url.path differ from the path that was actually requested. Middleware and endpoints that apply security restrictions based on request.url (rather than the raw scope path) could therefore be bypassed.

Details

When a client requests http://example.com/foo, it sends:

GET /foo HTTP/1.1
Host: example.com

Affected versions reconstructed the URL by concatenating http://{host}{path} and re-parsing the result. The Host value is only valid as a uri-host [ ":" port ] per RFC 9112 §3.2, where uri-host follows the restricted host grammar of RFC 3986 §3.2.2. When it contains characters outside that grammar - notably /, ?, or # - those characters move the path/query/fragment boundaries during re-parsing, so the parsed request.url.path no longer matches the path the server actually received. For example:

GET /foo HTTP/1.1
Host: example.com/abc?bar=

reconstructs to http://example.com/abc?bar=/foo, whose parsed path is /abc - even though routing used the real path /foo. The router still dispatches to /foo and the endpoint executes, but any middleware or code that reads request.url.path sees /abc, so path-based authorization checks can be bypassed.

Impact

Any application running an affected version that relies on request.url (or request.url.path) for security-sensitive decisions is affected. The most common case is middleware that gates access to certain path prefixes based on request.url.path. Deployments fronted by a proxy or load balancer are mitigated only if that proxy rejects or normalizes the malformed Host header before forwarding and the application does not trust attacker-controlled host headers (e.g. X-Forwarded-Host) elsewhere.

Mitigation

Upgrade to a patched version, which validates the Host header against the grammar of RFC 9112 §3.2 / RFC 3986 §3.2.2 when constructing request.url and falls back to scope["server"] for malformed values.

medium 5.3: CVE--2026--48817 Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')

Affected range<1.1.0
Fixed version1.1.0
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
EPSS Score0.213%
EPSS Percentile12th percentile
Description

Summary

When dispatching a request, HTTPEndpoint selects the handler by lowercasing the HTTP method and looking it up as an attribute with getattr, without restricting the lookup to a known set of HTTP verbs.

When an HTTPEndpoint subclass is registered through Route(...) without an explicit methods= argument, the route does not constrain the method and every method reaches the endpoint. If a non-standard HTTP method whose lowercased name matches an attribute on the endpoint subclass reaches the endpoint, that attribute is invoked as if it were a request handler. An attacker can use this to reach methods that were never meant to be HTTP handlers, such as internal helpers, without the authorization checks applied by the intended public handler.

Details

HTTPEndpoint uses the client-supplied method name to resolve an instance attribute, without validating it against the set of HTTP verbs the endpoint supports. A method such as _DO_DELETE therefore resolves an attribute like _do_delete and invokes it. Non-standard methods are valid RFC 9110 token methods, so an endpoint must not treat the method name as a trusted attribute selector.

Impact

An application is affected when all of the following hold:

  • It defines an HTTPEndpoint subclass and registers it via Route(...) without an explicit methods= argument.
  • The subclass defines additional methods whose names match a non-standard HTTP-method token shape and that accept a single request argument and return a response.

This also affects frameworks built on Starlette, like FastAPI.

Mitigation

Register HTTPEndpoint subclasses with an explicit methods= argument on the Route, listing only the HTTP verbs the endpoint supports. The route then rejects any other method with 405 Method Not Allowed before it reaches the endpoint, so non-standard methods cannot resolve an attribute.

low 3.7: CVE--2026--54282 Improper Input Validation

Affected range<1.3.0
Fixed version1.3.0
CVSS Score3.7
CVSS VectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:L/A:N
EPSS Score0.187%
EPSS Percentile9th percentile
Description

Summary

In affected versions, the HTTP request path is not validated before being used to reconstruct request.url. Because request.url is rebuilt by concatenating {scheme}://{host}{path} and re-parsing the result, a path that does not begin with / (for example @<!-- -->google.com) moves the authority boundary during re-parsing, so request.url.hostname and request.url.netloc become attacker-controlled. Code that reads request.url.hostname (rather than the Host header or scope) can therefore be misled into trusting an attacker-supplied host.

Details

When a client requests a path that does not start with /:

GET @<!-- -->google.com HTTP/1.1
Host: localhost

affected versions reconstruct the URL as http://localhost@<!-- -->google.com. Per RFC 3986 §3.2.1, the substring before @ in the authority is userinfo, so re-parsing yields username = "localhost" and hostname = "google.com", with an empty path:

request.url          == "http://localhost@<!-- -->google.com"
request.url.hostname == "google.com"
request.url.path     == ""

The root cause is that the path is concatenated directly after the host without a separating /, and without validating that it begins with one. Only the Host header was validated when constructing request.url; the path was not.

This requires an ASGI server that forwards a request-target lacking a leading / into scope["path"].

Impact

Any application running an affected version that uses request.url, request.url.netloc, or request.url.hostname for a security-sensitive decision (host-based authorization, redirect/callback base, SSRF target, cache key, audit log) may be affected, when no fronting proxy or load balancer rejects the malformed request-target first.

Note that this is less exploitable than GHSA-86qp-5c8j-p5mr: there, the poison is carried in the Host header, so the real path still routes to a valid endpoint while request.url.path lies. Here, the poison must be carried in the path itself, and that path (@<!-- -->google.com) does not match any registered route, so routing returns 404 and no endpoint handler runs. The exposure is limited to code that reads request.url before routing - notably middleware - or in 404/exception handlers.

Mitigation

Upgrade to a patched version, which prevents the request path from crossing into the URL authority. The request above instead yields http://localhost/@<!-- -->google.com with request.url.hostname == "localhost".

critical: 0 high: 1 medium: 0 low: 0 ddtrace 3.19.8 (pypi)

pkg:pypi/ddtrace@3.19.8

high 7.5: CVE--2026--50271 Uncontrolled Resource Consumption

Affected range<4.8.2
Fixed version4.8.2
CVSS Score7.5
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
EPSS Score0.441%
EPSS Percentile36th percentile
Description

Impact

Datadog tracing libraries that implement W3C baggage propagation parse incoming baggage HTTP headers without enforcing item-count or byte-size limits on the extract path. The DD_TRACE_BAGGAGE_MAX_ITEMS (default 64) and DD_TRACE_BAGGAGE_MAX_BYTES (default 8192) limits were applied only to baggage injection, not extraction. A remote, unauthenticated attacker can send a request whose baggage header contains an arbitrarily large number of comma-separated key-value pairs (or a single very large value). The tracer allocates a hash-map entry for each pair on every request, causing unbounded CPU and memory consumption and enabling a remote Denial of Service against any HTTP service that has the baggage propagation style enabled.
The baggage propagation style is enabled by default in most affected tracers, so any internet-facing service that has been instrumented with an affected tracer version is exposed unless the propagation style has been explicitly narrowed.

Patches

This is resolved in version 4.8.2 and later of the dd-trace-py library

Workarounds

If users cannot upgrade immediately:

  1. Disable baggage extraction by removing baggage from DD_TRACE_PROPAGATION_STYLE (or DD_TRACE_PROPAGATION_STYLE_EXTRACT if set independently).
  2. Cap the maximum HTTP request header size at an upstream proxy or web server (for example, Apache LimitRequestFieldSize, Nginx large_client_header_buffers, Envoy max_request_headers_kb).

Resources

Related upstream advisories:
opentelemetry-go GHSA-mh2q-q3fh-2475
opentelemetry-dotnet GHSA-g94r-2vxg-569j

critical: 0 high: 0 medium: 2 low: 0 github.com/quic-go/quic-go 0.54.1 (golang)

pkg:golang/github.com/quic-go/quic-go@0.54.1

medium 5.3: CVE--2026--40898 Allocation of Resources Without Limits or Throttling

Affected range<=0.59.0
Fixed version0.59.1
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
EPSS Score0.367%
EPSS Percentile29th percentile
Description

Summary

An attacker can cause excessive memory allocation in quic-go's HTTP/3 client and server implementations by sending a QPACK-encoded HEADERS frame that decodes into a large trailer field section with many unique field names and/or large values. The implementation builds an http.Header for the corresponding http.Request or http.Response, while only enforcing limits on the size of the QPACK-compressed HEADERS frame, not on the decoded field section. This can lead to memory exhaustion.

This is very similar to CVE-2025-64702. The difference is that this issue uses HTTP trailers, rather than HTTP headers, as the attack vector.

Impact

A misbehaving or malicious peer can cause a denial-of-service (DoS) attack against quic-go's HTTP/3 servers or clients by triggering excessive memory allocation, potentially leading to crashes or resource exhaustion. This affects both servers and clients due to symmetric header construction.

Details

In HTTP/3, field sections are compressed using QPACK (RFC 9204). Field sections are used for both HTTP headers and trailers. quic-go's HTTP/3 server and client decode the QPACK-encoded HEADERS frame into header fields, then construct an http.Request or http.Response.

http3.Server.MaxHeaderBytes and http3.Transport.MaxResponseHeaderBytes limit the encoded HEADERS frame size, with defaults of 1 MB for servers and 10 MB for clients. However, they did not limit the decoded field section size. A maliciously crafted HEADERS frame carrying trailers can expand to about 50x the encoded size using QPACK static table entries with long names and/or values.

RFC 9114 requires endpoints to enforce decoded field section size limits via SETTINGS, which quic-go did not do for trailers.

The Fix

quic-go now enforces RFC 9114 decoded field section size limits for trailers as well. It incrementally decodes QPACK entries and checks the field section size after each entry, aborting the stream if an entry causes the limit to be exceeded.

medium 5.3: CVE--2025--64702 Allocation of Resources Without Limits or Throttling

Affected range<0.57.0
Fixed version0.57.0
CVSS Score5.3
CVSS VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
EPSS Score0.338%
EPSS Percentile26th percentile
Description

Summary

An attacker can cause excessive memory allocation in quic-go's HTTP/3 client and server implementations by sending a QPACK-encoded HEADERS frame that decodes into a large header field section (many unique header names and/or large values). The implementation builds an http.Header (used on the http.Request and http.Response, respectively), while only enforcing limits on the size of the (QPACK-compressed) HEADERS frame, but not on the decoded header, leading to memory exhaustion.

Impact

A misbehaving or malicious peer can cause a denial-of-service (DoS) attack on quic-go's HTTP/3 servers or clients by triggering excessive memory allocation, potentially leading to crashes or exhaustion. It affects both servers and clients due to symmetric header construction.

Details

In HTTP/3, headers are compressed using QPACK (RFC 9204). quic-go's HTTP/3 server (and client) decodes the QPACK-encoded HEADERS frame into header fields, then constructs an http.Request (or response).

http3.Server.MaxHeaderBytes and http3.Transport.MaxResponseHeaderBytes, respectively, limit encoded HEADERS frame size (default: 1 MB server, 10 MB client), but not decoded size. A maliciously crafted HEADERS frame can expand to ~50x the encoded size using QPACK static table entries with long names / values.

RFC 9114 requires enforcing decoded field section size limits via SETTINGS, which quic-go did not do.

The Fix

quic-go now enforces RFC 9114 decoded field section size limits, sending SETTINGS_MAX_FIELD_SECTION_SIZE and using incremental QPACK decoding to check the header size after each entry, aborting early on violations with HTTP 431 (on the server side) and stream reset (on the client side).

critical: 0 high: 0 medium: 1 low: 0 busybox 1.37.0-r30 (apk)

pkg:apk/alpine/busybox@1.37.0-r30?os_name=alpine&os_version=3.23

medium : CVE--2025--60876

Affected range<=1.37.0-r30
Fixed versionNot Fixed
EPSS Score0.285%
EPSS Percentile21st percentile
Description
critical: 0 high: 0 medium: 1 low: 0 go.opentelemetry.io/otel 1.43.0 (golang)

pkg:golang/go.opentelemetry.io/otel@1.43.0

medium : CVE--2026--41178

Affected range>=1.43.0
<1.44.0
Fixed version1.44.0
EPSS Score0.237%
EPSS Percentile15th percentile
Description

Opentelemetry-go's baggage parsing no longer caps raw header length in go.opentelemetry.io/otel

critical: 0 high: 0 medium: 0 low: 2 github.com/refraction-networking/utls 1.7.3 (golang)

pkg:golang/github.com/refraction-networking/utls@1.7.3

low 2.3: CVE--2026--27017 Use of a Cryptographic Primitive with a Risky Implementation

Affected range>=1.6.0
<1.8.1
Fixed version1.8.1
CVSS Score2.3
CVSS VectorCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
EPSS Score0.154%
EPSS Percentile5th percentile
Description

There is a fingerprint mismatch with Chrome when using GREASE ECH, having to do with ciphersuite selection. When Chrome selects the preferred ciphersuite in the outer ClientHello and the ciphersuite for ECH, it does so consistently based on hardware support. That means, for example, if it prefers AES for the outer ciphersuite, it would also use AES for ECH. The Chrome parrot in utls hardcodes AES preference for outer ciphersuites but selects the ECH ciphersuite randomly between AES and ChaCha20. So there is a 50% chance of selecting ChaCha20 for ECH while using AES for the outer ciphersuite, which is impossible in Chrome.

This is only a problem in GREASE ECH, since in real ECH Chrome selects the first valid ciphersuite when AES is preferred, which is the same in utls. So no change is done there.

Affected symbols: HelloChrome_120, HelloChrome_120_PQ, HelloChrome_131, HelloChrome_133

Fix commit: 24bd1e05a788c1add7f3037f4532ea552b2cee07

Thanks to telegram @acgdaily for reporting this issue.

low 2.3: CVE--2026--26995 Exposure of Sensitive Information to an Unauthorized Actor

Affected range>=1.6.0
<1.8.2
Fixed version1.8.2
CVSS Score2.3
CVSS VectorCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N
Description

The padding extension was incorrectly removed in utls for the non-pq variant of Chrome 120 fingerprint. Chrome removed this extension only when sending pq keyshares. Only this fingerprint is affected since newer fingerprints have pq keyshares by default and older fingerprints have this extension.

Affected symbols: HelloChrome_120

Fix commit: 8fe0b08e9a0e7e2d08b268f451f2c79962e6acd0

Thanks to telegram @acgdaily for reporting this issue.

critical: 0 high: 0 medium: 0 low: 1 github.com/cloudflare/circl 1.6.1 (golang)

pkg:golang/github.com/cloudflare/circl@1.6.1

low 2.9: CVE--2026--1229 Incorrect Calculation

Affected range<1.6.3
Fixed version1.6.3
CVSS Score2.9
CVSS VectorCVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:L/SI:L/SA:L/E:P/S:N/AU:Y/U:Amber
EPSS Score0.397%
EPSS Percentile32nd percentile
Description

The CombinedMult function in the CIRCL ecc/p384 package (secp384r1 curve) produces an incorrect value for specific inputs. The issue is fixed by using complete addition formulas.
ECDH and ECDSA signing relying on this curve are not affected.

The bug was fixed in v1.6.3.

critical: 0 high: 0 medium: 0 low: 0 unspecified: 1golang.org/x/net 0.55.0 (golang)

pkg:golang/golang.org/x/net@0.55.0

unspecified : CVE--2026--46600

Affected range<0.56.0
Fixed version0.56.0
EPSS Score0.339%
EPSS Percentile26th percentile
Description

Parsing an invalid SVCB or HTTPS RR can panic when the size of a parameter value overflows the message buffer.

critical: 0 high: 0 medium: 0 low: 0 unspecified: 1golang.org/x/crypto 0.53.0 (golang)

pkg:golang/golang.org/x/crypto@0.53.0

unspecified : GO--2026--5932

Affected range>=0
Fixed versionNot Fixed
Description

The golang.org/x/crypto/openpgp package is unsafe by design, has numerous known security issues, is not maintained, and should not be used.

If you are required to interoperate with OpenPGP systems and need a maintained package, consider github.com/ProtonMail/go-crypto/openpgp which is a maintained fork that aims to be a drop-in replacement for this package.

critical: 0 high: 0 medium: 0 low: 0 unspecified: 1github.com/klauspost/compress 1.18.5 (golang)

pkg:golang/github.com/klauspost/compress@1.18.5

unspecified : GHSA--259r--337f--4rfw

Affected range>=1.16.0
<1.18.7
Fixed version1.18.7
Description

Providing a specially crafted dictionary to s2.NewDict and using it to encode data can make the encoder read out of bounds.

critical: 0 high: 0 medium: 0 low: 0 unspecified: 1golang.org/x/text 0.38.0 (golang)

pkg:golang/golang.org/x/text@0.38.0

unspecified : CVE--2026--56852

Affected range<0.39.0
Fixed version0.39.0
EPSS Score0.446%
EPSS Percentile37th percentile
Description

A norm.Iter can enter an infinite loop when handling input containing invalid UTF-8 bytes.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown

🔍 Vulnerabilities of permitio/pdp-v2:next

📦 Image Reference permitio/pdp-v2:next
digestsha256:5da584aba45bfe2873d2c4407ecc37af100219c6a83671233cc65a5f63d7fa09
vulnerabilitiescritical: 0 high: 0 medium: 0 low: 0
platformlinux/amd64
size133 MB
packages250
📦 Base Image python:3.13-alpine3.23
also known as
  • 3.13.14-alpine3.23
  • e0e75f8d10947da66ae425727cad4e480c65f32018367f4006f9eba40c48cd5c
digestsha256:72c39ab9dbf2227aa91ec2246e6492260ee2530c36bdf37b208394b42d757b60
vulnerabilitiescritical: 0 high: 2 medium: 1 low: 0

Comment thread Dockerfile
# 3.13.14 - that is the floor for the fixes above.
#
# Python 3.10 also reaches end of life in October 2026, so this move was due regardless.
FROM python:3.13-alpine3.23 AS main

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You updated from 3.10 to 3.13 ? this probably needs some verifications that every package we use is compatible with it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right that this needed checking — and it turned out CI wasn't checking it at all. The pytests job pinned Python 3.11.8 while the image ran 3.10, so the unit suite had never executed on the shipped interpreter. Pushed 240c74e to run it on 3.13 so the two stay in lockstep.

Verified before and after that change:

In CI (run 30533253549)

  • pytests on CPython 3.13.14 — 118 passed
  • build-pdp-image — all deps install on musl/alpine 3.23; pydantic 1.10.26 Cython-compiles from source
  • pdp-tester — 24/24 e2e against the built 3.13 image (enforcement rbac/rebac/derivation, user_permissions, sync_facts, health, logs), no retries

Against the real artifact — pulled the pdp-image artifact so this is the shipped image, not a re-resolve

  • 118 unit tests pass inside the amd64 image — musl, its own dependency set, setuptools absent
  • 118 pass on a native arm64 musl build of the Python layers; CI only builds linux/amd64, so that surface had no coverage
  • Image confirms Python 3.13.14 / expat_2.8.1 — the CVE fixes are in the artifact, not just in the advisory ranges
  • All 86 installed top-level packages import

Static

  • Nothing in horizon/ uses an API changed or removed in 3.12/3.13 — no datetime.utcnow, bare get_event_loop(), locale.getdefaultlocale, ssl.wrap_socket, or removed ast/pkgutil spellings. The only DeprecationWarning we raise is FastAPI's on_event at horizon/pdp.py:446-447, which fires on 3.10 too.
  • One non-obvious risk specific to this image: 3.12 removed distutils from the stdlib, and the Dockerfile uninstalls setuptools, so there's no _distutils_hack shim either — a stray runtime import distutils that worked on 3.10 is a hard failure here. Verified the runtime import graph is clean with distutils/setuptools/pkg_resources blocked; the remaining references in deps (cffi shims, ddtrace's vendored pybind11/psutil setup helpers) are build-time only.

Two things this turned up that aren't about Python, both better as separate tickets than as scope here:

  1. websockets isn't pinned, and it drifted mid-review. The build that first validated this PR shipped 16.1.1; 17.0 published a few hours later, so any rebuild picked up a new major on the OPAL pubsub path with nothing capping it (fastapi-websocket-pubsub allows >=14.0). The current run does build and e2e-test 17.0, so we're covered now — but 18.0 can do the same thing. Same class as the starlette/ddtrace drift the requirements.txt comments describe.
  2. test_warn_if_opal_verifier_disabled_fires is environment-dependent. It passes in CI but fails when run inside the image: the image bakes OPAL_AUTH_PUBLIC_KEY (Dockerfile:229), so the verifier is enabled and the warning correctly doesn't fire — the test's premise at line 112 only holds outside the image.

@omer9564 omer9564 Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in horizon/ uses an API changed or removed in 3.12/3.13 — no datetime.utcnow, bare get_event_loop(), locale.getdefaultlocale, ssl.wrap_socket, or removed ast/pkgutil spellings. The only DeprecationWarning we raise is FastAPI's on_event at horizon/pdp.py:446-447, which fires on 3.10 too.

Verify this statement is true also for the used opal-client and opal-common packages ( including fastapi websocket pubsub and rpc packages ) - if possible just check the entire dependency tree somehow

websockets isn't pinned, and it drifted mid-review. The build that first validated this PR shipped 16.1.1; 17.0 published a few hours later, so any rebuild picked up a new major on the OPAL pubsub path with nothing capping it (fastapi-websocket-pubsub allows >=14.0). The current run does build and e2e-test 17.0, so we're covered now — but 18.0 can do the same thing. Same class as the starlette/ddtrace drift the requirements.txt comments describe.

Lets pin it - we don't know what major side effects this can have

Zivxx and others added 2 commits July 30, 2026 13:04
The pytests job pinned Python 3.11.8 while the image ran 3.10, so the unit
suite has never executed on the shipped interpreter and a regression that
only reproduces there could not fail CI. Moving the image to 3.13 widened
that gap, which is what Omer flagged in review on #329.

Verified before changing it, against the real artifact (the pdp-image from
run 30447703110, the same run whose pdp-tester passed):

  - 118/118 unit tests inside the real amd64 image - musl, its own dependency
    resolution, no setuptools shim
  - 118/118 on a native arm64 build of the Python layers, which CI never
    builds (linux/amd64 only); pydantic 1.10.26 Cython-compiles from source
    on musl/arm64 against 3.13
  - image confirms Python 3.13.14 and expat 2.8.1 - the CVE fixes are in the
    artifact, not just the advisory ranges
  - no code in horizon/ uses any API changed or removed in 3.12/3.13; the only
    DeprecationWarning we raise is FastAPI's on_event, which fires on 3.10 too

Patch version floats, matching the Dockerfile base, so the two move together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing else bounds websockets: opal-common/opal-client and fastapi-websocket-rpc
allow >=10.3, fastapi-websocket-pubsub >=14.0, uvicorn[standard] >=13.0. With no
lockfile, a rebuild silently absorbs new majors on the OPAL pub/sub path.

That happened during review of #329. The CI build that first validated the Python
3.13 move shipped websockets 16.1.1; 17.0 published a few hours later, so the next
rebuild would have carried an unvalidated major to a release. Pinning to 17.0 rather
than 16.1.1 because 17.0 is the version run 30533253549 actually validated: 24/24
pdp-tester e2e against the built image, plus 118 unit tests on CPython 3.13.14.

Verified the pin resolves against every lower bound above and that the suite still
passes on the pinned tree (118/118, arm64 musl, 3.13.14).

Requested by @omer9564 in review.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Zivxx
Zivxx merged commit 743a172 into main Jul 30, 2026
8 of 9 checks passed
@Zivxx
Zivxx deleted the july-29-vulnerability-fixes branch July 30, 2026 12:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants