Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 33 additions & 15 deletions .docker/scout/pdp-v2.vex.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,46 @@
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://openvex.dev/docs/permitio-pdp/pdp-v2-cve-waivers",
"author": "Permit.io",
"timestamp": "2026-07-29T00:00:00Z",
"version": 4,
"timestamp": "2026-09-15T00:00:00Z",
"version": 10,
"statements": [
{
"vulnerability": {
"name": "CVE-2026-48818"
"name": "CVE-2026-78662"
},
"products": [
{
"@id": "pkg:docker/permitio/pdp-v2@next",
"subcomponents": [
{ "@id": "pkg:pypi/starlette@0.50.0" }
{ "@id": "pkg:golang/golang.org/x/crypto@0.53.0" },
{ "@id": "pkg:golang/golang.org/x/crypto@0.55.0" }
]
}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "CVE-2026-48818 is an SSRF / NTLM credential-theft issue in starlette.staticfiles that only triggers on Windows, via UNC-path resolution when serving a StaticFiles mount. The PDP runs exclusively on Linux (Alpine) and mounts no StaticFiles anywhere in the app (only API routers are mounted), so the vulnerable code path is never present or executed. The fix lands only in starlette >= 1.1.0, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
"justification": "vulnerable_code_not_present",
"impact_statement": "CVE-2026-78662 is a connection-deadlock DoS - a channel registered in the mux's chanList before it is established lets a malicious peer flood incomingRequests in golang.org/x/crypto/ssh. It reaches the pdp-v2 image only because /app/bin/opa is compiled from permitio/permit-opa, which carries x/crypto as an INDIRECT dependency. The vulnerable package is not present in the binary: `go list -deps ./cmd/opa` in permit-opa resolves exactly these x/crypto packages - cryptobyte(+asn1), chacha20, chacha20poly1305, internal/poly1305, internal/alias, pbkdf2, curve25519, blake2b, salsa20/salsa, nacl/box, nacl/secretbox, hkdf, sha3 - and golang.org/x/crypto/ssh is absent from that list, so the SSH transport code is never linked, let alone reachable. The PDP additionally speaks no SSH on any code path of its own. The justification is `vulnerable_code_not_present` rather than `vulnerable_code_not_in_execute_path` on purpose: the x/crypto MODULE is present (cryptobyte, chacha20, curve25519 and the rest are linked), but the ssh package's code is excluded at link time by Go's import graph, which is what OpenVEX defines that label to mean. `not_in_execute_path` would assert the weaker and different claim that the ssh code ships and merely is not called. Both CVEs are fixed only in x/crypto >= 0.56.0, whose go.mod declares `go 1.26.0`. That blocker is LOCAL, not upstream: Go 1.26 has been stable since 2026-02-10 (1.26.8 current, 1.27.0 released) and golang:1.26-bookworm exists, but this image still builds its OPA on golang:1.25-bookworm (the opa_build stage) and permit-opa's go.mod is still `go 1.25.0`. The official golang images ship GOTOOLCHAIN=local, so a 1.25 builder hard-fails rather than fetching a newer toolchain, which is why the base bump has to land first. permitio/permit-opa#49 raises the floor as far as Go 1.25 allows (0.55.0, which clears CVE-2026-56854) and bumps grpc to 1.83.2. Two subcomponent PURLs are listed on purpose: 0.53.0 is what the build resolves until permit-opa#49 merges and 0.55.0 is what it resolves after, and docker scout matches VEX subcomponents on the FULL PURL string, so a statement naming only one of them silently stops suppressing the moment the version moves. Note also that scout emits Go PURLs WITHOUT the conventional `v` prefix (pkg:golang/golang.org/x/crypto@0.53.0, not @v0.53.0) - the first version of this waiver used @v0.55.0 and bound to nothing at all. Removal gate, both closable inside Permit: land PDP#334 (moves the OPA builder to golang:1.26-bookworm), then move permit-opa's go directive to 1.26.0 and x/crypto to >= 0.56.0, then delete this statement. Nothing here waits on Go. Tracked under PER-15358."
},
{
"vulnerability": {
"name": "CVE-2026-54283"
"name": "CVE-2026-56855"
},
"products": [
{
"@id": "pkg:docker/permitio/pdp-v2@next",
"subcomponents": [
{ "@id": "pkg:golang/golang.org/x/crypto@0.53.0" },
{ "@id": "pkg:golang/golang.org/x/crypto@0.55.0" }
]
}
],
"status": "not_affected",
"justification": "vulnerable_code_not_present",
"impact_statement": "CVE-2026-56855 is a connection-deadlock DoS - crafted post-establishment channel messages are buffered instead of being treated as a protocol error in golang.org/x/crypto/ssh. It reaches the pdp-v2 image only because /app/bin/opa is compiled from permitio/permit-opa, which carries x/crypto as an INDIRECT dependency. The vulnerable package is not present in the binary: `go list -deps ./cmd/opa` in permit-opa resolves exactly these x/crypto packages - cryptobyte(+asn1), chacha20, chacha20poly1305, internal/poly1305, internal/alias, pbkdf2, curve25519, blake2b, salsa20/salsa, nacl/box, nacl/secretbox, hkdf, sha3 - and golang.org/x/crypto/ssh is absent from that list, so the SSH transport code is never linked, let alone reachable. The PDP additionally speaks no SSH on any code path of its own. The justification is `vulnerable_code_not_present` rather than `vulnerable_code_not_in_execute_path` on purpose: the x/crypto MODULE is present (cryptobyte, chacha20, curve25519 and the rest are linked), but the ssh package's code is excluded at link time by Go's import graph, which is what OpenVEX defines that label to mean. `not_in_execute_path` would assert the weaker and different claim that the ssh code ships and merely is not called. Both CVEs are fixed only in x/crypto >= 0.56.0, whose go.mod declares `go 1.26.0`. That blocker is LOCAL, not upstream: Go 1.26 has been stable since 2026-02-10 (1.26.8 current, 1.27.0 released) and golang:1.26-bookworm exists, but this image still builds its OPA on golang:1.25-bookworm (the opa_build stage) and permit-opa's go.mod is still `go 1.25.0`. The official golang images ship GOTOOLCHAIN=local, so a 1.25 builder hard-fails rather than fetching a newer toolchain, which is why the base bump has to land first. permitio/permit-opa#49 raises the floor as far as Go 1.25 allows (0.55.0, which clears CVE-2026-56854) and bumps grpc to 1.83.2. Two subcomponent PURLs are listed on purpose: 0.53.0 is what the build resolves until permit-opa#49 merges and 0.55.0 is what it resolves after, and docker scout matches VEX subcomponents on the FULL PURL string, so a statement naming only one of them silently stops suppressing the moment the version moves. Note also that scout emits Go PURLs WITHOUT the conventional `v` prefix (pkg:golang/golang.org/x/crypto@0.53.0, not @v0.53.0) - the first version of this waiver used @v0.55.0 and bound to nothing at all. Removal gate, both closable inside Permit: land PDP#334 (moves the OPA builder to golang:1.26-bookworm), then move permit-opa's go directive to 1.26.0 and x/crypto to >= 0.56.0, then delete this statement. Nothing here waits on Go. Tracked under PER-15358."
},
{
"vulnerability": {
"name": "CVE-2026-48818"
},
"products": [
{
Expand All @@ -35,11 +53,11 @@
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "CVE-2026-54283 causes request.form() size limits to be silently ignored for application/x-www-form-urlencoded bodies, enabling a memory-exhaustion DoS. The PDP is a JSON-only API: it never calls request.form(), does not depend on python-multipart, and parses no form-encoded or multipart request bodies, so the vulnerable form-parsing path is never executed. The fix lands only in starlette >= 1.3.1, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
"impact_statement": "CVE-2026-48818 is an SSRF / NTLM credential-theft issue in starlette.staticfiles that only triggers on Windows, via UNC-path resolution when serving a StaticFiles mount. The PDP runs exclusively on Linux (Alpine) and mounts no StaticFiles anywhere in the app (only API routers are mounted), so the vulnerable code path is never present or executed. The fix lands only in starlette >= 1.1.0, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
},
{
"vulnerability": {
"name": "CVE-2026-48710"
"name": "CVE-2026-54283"
},
"products": [
{
Expand All @@ -51,11 +69,11 @@
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "CVE-2026-48710 lets a malformed HTTP Host header make request.url.path diverge from the path actually routed, so security checks that read request.url can be bypassed. The PDP performs no security decision on request.url: authentication is enforced per-route through FastAPI Depends security dependencies (HTTPBearer in horizon/authentication.py), not by path matching, and the app registers no add_middleware/BaseHTTPMiddleware and reads no raw ASGI scope paths. The one path-keyed authorization check - the write-route test in horizon/proxy/api.py cloud_proxy - reads request.path_params, which Starlette's router populates from the raw scope path and which this CVE explicitly leaves unaffected. The fix lands only in starlette >= 1.0.1, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
"impact_statement": "CVE-2026-54283 causes request.form() size limits to be silently ignored for application/x-www-form-urlencoded bodies, enabling a memory-exhaustion DoS. The PDP is a JSON-only API: it never calls request.form(), does not depend on python-multipart, and parses no form-encoded or multipart request bodies, so the vulnerable form-parsing path is never executed. The fix lands only in starlette >= 1.3.1, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
},
{
"vulnerability": {
"name": "CVE-2026-48817"
"name": "CVE-2026-48710"
},
"products": [
{
Expand All @@ -67,23 +85,23 @@
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "CVE-2026-48817 is an unsafe-reflection issue in starlette.endpoints.HTTPEndpoint request dispatch: the HTTP method is lowercased and resolved with getattr against the endpoint instance without restricting the lookup to known HTTP verbs, letting an attacker invoke internal helper methods that bypass authorization. The PDP defines no HTTPEndpoint or WebSocketEndpoint subclass anywhere in the codebase - every route is registered through FastAPI APIRouter decorators with explicit HTTP methods - so the vulnerable dispatch class is never instantiated or reached. The fix lands only in starlette >= 1.1.0, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
"impact_statement": "CVE-2026-48710 lets a malformed HTTP Host header make request.url.path diverge from the path actually routed, so security checks that read request.url can be bypassed. The PDP performs no security decision on request.url: authentication is enforced per-route through FastAPI Depends security dependencies (HTTPBearer in horizon/authentication.py), not by path matching. The app is not middleware-free, as an earlier version of this statement claimed: horizon/pdp.py adopts OPAL's FastAPI app, and opal_client/client.py calls configure_middleware, which registers Starlette's CORSMiddleware at opal_common/middleware.py:77. That does not put the PDP on the vulnerable path - CORSMiddleware dispatches solely on the Origin header and scope['method'] (starlette/middleware/cors.py reads no `path`, no `url`, and never constructs a Request), so it cannot observe the divergence this CVE induces between request.url.path and the routed path. It is the only add_middleware call in the image; no BaseHTTPMiddleware is registered anywhere, and no first-party code reads raw ASGI scope paths. The one path-keyed authorization check - the write-route test in horizon/proxy/api.py cloud_proxy - reads request.path_params, which Starlette's router populates from the raw scope path and which this CVE explicitly leaves unaffected. The fix lands only in starlette >= 1.0.1, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
},
{
"vulnerability": {
"name": "CVE-2026-15308"
"name": "CVE-2026-48817"
},
"products": [
{
"@id": "pkg:docker/permitio/pdp-v2@next",
"subcomponents": [
{ "@id": "pkg:generic/python" }
{ "@id": "pkg:pypi/starlette@0.50.0" }
]
}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"impact_statement": "CVE-2026-15308 is a CPU denial-of-service in CPython's incremental HTML parser: html.parser.HTMLParser degrades to quadratic complexity on repeated unterminated markup declarations, so parsing attacker-controlled HTML can exhaust CPU. The PDP never parses HTML. Nothing imports html.parser - neither the PDP's own code (horizon/, pdp-server/) nor any package installed in the image; this was verified by scanning every .py file under site-packages, so the vulnerable class is never instantiated. Unlike the other CPython findings in this scan (CVE-2026-6019 and CVE-2026-7210, both cleared by moving the base image to Python 3.13.14), this one cannot be upgraded away: PSF fixed it only in 3.15.0b4 and NVD's CPE range is < 3.15.0, so no released Python satisfies it and every Python-based image in the world matches today. The Dockerfile intentionally floats the base image patch version (python:3.13-alpine3.23), so when 3.13.15 ships with the backport a rebuild will pick it up automatically. Remove this waiver at that point. Tracked under PER-15358."
"impact_statement": "CVE-2026-48817 is an unsafe-reflection issue in starlette.endpoints.HTTPEndpoint request dispatch: the HTTP method is lowercased and resolved with getattr against the endpoint instance without restricting the lookup to known HTTP verbs, letting an attacker invoke internal helper methods that bypass authorization. The PDP defines no HTTPEndpoint or WebSocketEndpoint subclass anywhere in the codebase - every route is registered through FastAPI APIRouter decorators with explicit HTTP methods - so the vulnerable dispatch class is never instantiated or reached. The fix lands only in starlette >= 1.1.0, but opal-common/opal-client 0.9.6 cap starlette<1, so the PDP is pinned to 0.50.0 (see requirements.txt). This waiver is temporary and must be removed once OPAL relaxes its starlette<1 bound and starlette is upgraded to >= 1.3.1. Tracked under PER-15358."
},
{
"vulnerability": {
Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,47 @@ jobs:
tags: permitio/pdp-v2:${{ github.event.release.tag_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Never serve the `main` stage from the GHA cache. Its cache key is the instruction
# text plus the parent layer, so when the base image digest has NOT moved but
# Alpine's package index has, a release cut later replays the old
# `apk update && apk upgrade` + `pip install` results and re-ships packages
# upstream has since patched. That is the shape of PER-15358: 0.9.14 froze
# libcrypto3 3.5.7-r0 on 2026-08-04 and Alpine published 3.5.8-r0 afterwards.
#
# NOTE the input is PLURAL. `no-cache-filter` (singular) is the buildx CLI flag;
# the action input is `no-cache-filters`, and an undeclared input is warned about
# and dropped - i.e. the singular spelling makes this silently inert.
#
# `opa_build` is deliberately NOT listed: `COPY custom* /custom` pulls in
# custom_opa.tar.gz, which the Pre-build step regenerates every run, so that stage
# already re-executes unconditionally. Listing it would imply this filter controls
# the OPA binary's Go dependency versions, and it does not - those come from
# permit-opa's go.sum (see permitio/permit-opa#49).
#
# The Rust stages are untouched by this filter, and in practice they barely cache at
# all: `rust_planner`'s `COPY . .` sees the regenerated tarball, so it and the final
# `cargo zigbuild` re-run every build. `cargo chef cook` re-runs too despite being
# keyed on recipe.json - 404.6s on tests.yml run 34858998837. The likely cause is
# that no Cargo.lock is tracked (it is also .dockerignore'd), so `cargo chef prepare`
# resolves crates.io fresh each build and the lock embedded in recipe.json drifts.
# Do not budget this filter against a cached dependency compile.
#
# READ THAT MEASUREMENT NARROWLY: it comes from a `pull_request` run of tests.yml
# built `--platform linux/amd64`. The rust stages are `--platform=$BUILDPLATFORM` so
# their cost carries over, but THIS step builds amd64+arm64, so opa_build and main
# are each built twice with the second leg under QEMU - and a tag ref restores
# different GHA caches than a PR ref. Do not read that run's cache profile, or any
# "longest step" ranking from it, as this step's.
#
# On that run `rust_chef` cached three of its four layers (#15-#17); #18
# `rustup target add` re-ran. Its `apk add musl-dev openssl-dev zig pkgconf perl make`
# is a package-resolving layer of exactly the class this filter exists to de-stale,
# sitting outside the filter. Impact is limited because pdp-server vendors OpenSSL
# (pdp-server/Cargo.toml) rather than linking that cached openssl-dev, so the
# staleness stays in the build toolchain and never reaches the shipped image. That
# also makes `openssl-dev` and `ENV OPENSSL_DIR=/usr` dead config; removing them is
# out of scope here and tracked separately under PER-15358.
no-cache-filters: main

- name: Build and push PDP image - (official release)
if: "!github.event.release.prerelease"
Expand All @@ -74,6 +115,10 @@ jobs:
tags: permitio/pdp-v2:${{ github.event.release.tag_name }},permitio/pdp-v2:latest
cache-from: type=gha
cache-to: type=gha,mode=max
# See the pre-release step above: forces `apk upgrade` and `pip install` to
# re-resolve so a release can never republish a stale OS/dependency set.
# Plural input, and `main` only - both for the reasons documented there.
no-cache-filters: main

update-pdp-api-ecs-service:
needs: build-and-push-pdp
Expand Down
Loading
Loading