Skip to content

Fix the persisted-query bypass, CIDR allowed_clients and body-phase identity - #8

Merged
raffaelschneider merged 4 commits into
mainfrom
fix/graphql-security
Oct 22, 2026
Merged

raffaelschneider merged 4 commits into
mainfrom
fix/graphql-security

Conversation

@raffaelschneider

Copy link
Copy Markdown
Contributor

Fixes #5.

  1. Persisted queries:
    • The agent hashes the query text it received and rejects a client hash that disagrees. Before, a malicious query sent under an allowed hash passed.
    • Hash-only requests take their text from a server-side store: the allowlist file (entries may now carry query), or a capped APQ cache of verified queries.
    • An unknown hash gets PERSISTED_QUERY_NOT_FOUND; a hash that isn't allowlisted gets QUERY_NOT_ALLOWED.
  2. Body analysis saw no headers and client IP "unknown": this was why introspection allowlists could never match and why field auth denied every field a rule matched. The headers event now stores headers and the metadata client IP by correlation ID (60 s lifetime, capped); with no headers event, nothing is granted.
  3. allowed_clients:
    • IPs and CIDRs match the client IP, including IPv4-mapped addresses.
    • Also fixed a second bypass: with allowed_clients_header set, sending an allowed IP such as 127.0.0.1 as that header's value granted introspection.
  4. Tests: a config push that enables persisted queries loads the allowlist, and an unloadable allowlist rejects the configure. The underlying fix came in with Make configure idempotent, merging and strict #7.

Rebased on #7 (idempotent configure); conflicts in README and agent.rs were resolved keeping both sides. 98 tests pass; the new bypass tests fail on the old code.

E2e with real Zentinel: allowed queries pass with text, hash, or hash only. Another query under the allowed hash, an unknown hash, and a query not on the allowlist are all refused with QUERY_NOT_ALLOWED.

The allowlist checked the client-supplied extensions.persistedQuery.sha256Hash
instead of hashing the query it received, so any query passed under a
listed hash. The agent now hashes the received text itself and rejects a
client hash that disagrees.

Hash-only (APQ) requests are resolved to query text from a server-side
store - the allowlist file (entries may now carry the query text) or, in
cache mode, queries seen earlier with a verified hash - and every analyzer
checks the resolved text. Unknown hashes get PERSISTED_QUERY_NOT_FOUND.
Cache mode previously did nothing.

Refs #5
The body event carries only the correlation ID, and the agent analyzed it
with no headers and the client IP "unknown". Introspection allowed_clients
(by IP or by header) could therefore never match, and field_auth denied
every field a rule matched whatever roles or scopes the caller had.

The headers event now records the headers (names lowercased) and the
client IP from the event metadata, keyed by correlation ID, and the last
body chunk takes them. Entries expire after 60 s and the map is bounded;
a body without a recorded headers event is analyzed with none, which only
denies.

Refs #5
allowed_clients was an exact string comparison, so ranges never matched
and an IPv4-mapped address missed its IPv4 entry. IP and CIDR entries are
now parsed once and matched against the client address; other entries are
header secrets for allowed_clients_header.

The single list also meant that, with allowed_clients_header set, sending
an allowed IP (e.g. 127.0.0.1) as the header value granted introspection
to anyone. IP and range entries are no longer accepted as header values.

Refs #5
Before #7, on_configure rebuilt the persisted-query analyzer without
reading allowlist_file, so after a config push the allowlist was empty
and every query was blocked. #7 fixed that by loading the file in
apply_configure and rejecting the push if the file cannot be loaded.
These tests pin that down for the hash-verifying allowlist:

- a push that enables persisted queries loads the file: listed queries
  pass by text and hash-only, a spoofed hash is refused, and the same
  push repeated keeps working;
- a push naming an unreadable file, or an entry whose hash does not match
  its query, is rejected and the running allowlist stays.

Fixes #5
@raffaelschneider
raffaelschneider merged commit 9cdf7fb into main Oct 22, 2026
3 checks passed
@raffaelschneider
raffaelschneider deleted the fix/graphql-security branch October 22, 2026 07:15
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.

Persisted-query allowlist can be bypassed

1 participant