Skip to content

[Security] low: GraphQL field-suggestion ("Did you mean") schema disclosure despite introspection disabled in prod #475

Description

@Kravalg

Summary

Production hardens GraphQL by disabling introspection (api_platform.yaml when@prod: introspection.enabled:false) and graphiql. However introspection disabling only blocks __schema/__type meta-queries; it does not disable webonyx/graphql-php's document-validation suggestions. The FieldsOnCorrectType / fields-and-types validation rules run regardless and emit "Cannot query field X on type Y. Did you mean Z?" and type-name suggestions. API Platform 4.1 exposes these validation error messages verbatim on the public POST /api/graphql endpoint with no custom error formatter registered to strip them (no formatError/error-handler override exists anywhere in config or src/Shared). An attacker can thus reconstruct field and type names one probe at a time even though introspection appears locked down, defeating the intended controls.

Severity: LOW • Category: A05:2021 Security Misconfiguration (information disclosure) • OWASP: A05:2021; API8:2023
Found by an authorized automated adversarial pentest loop and confirmed by 2 independent skeptic verifiers (unanimous).

Affected code / location

config/packages/api_platform.yaml:8-11,49-53 (graphiql:false, introspection.enabled:false in prod, no error/validation sanitization); config/packages/security.yaml:16 (^/api/graphql POST PUBLIC_ACCESS)

Exploit scenario

  1. POST to /api/graphql (public) a query referencing a deliberately misspelled field, e.g. { userr { ide } }. 2. The response validation error returns Did you mean "user"? / Did you mean "id"?, leaking valid field names. 3. Iterate the guess-and-suggest oracle across types/fields/mutations to rebuild the schema (mutation names, argument fields) without ever calling __schema — enabling targeted attacks against operations that assume schema opacity in prod.

Suggested remediation

Register a production GraphQL error/validation handler that suppresses field/type suggestion text (or disables the QuerySuggestion behavior in webonyx) so validation errors return generic messages, consistent with introspection being off.

Verification notes

  • Verifier 1 (CONFIRMED, exploitable=true, duplicate=false): Traced a concrete, currently-reachable exploit path. (1) api_platform.yaml:49-53 disables only introspection.enabled and graphiql in prod; with webonyx/graphql-php ^15.17 this installs the DisableIntrospection rule, which blocks __schema/__type only. The FieldsOnCorrectType validation rule still runs and emits "Cannot query field X on type Y. Did you mean Z?" suggestions; graphql-php has no toggle for these and no custom validation rule is registered (grep of config/ and src/Shared shows none). (2) security.yaml:16 makes POST /api/graphql PUBLIC_ACCESS, so probing needs no auth. (3) No formatter strips validation-error messages: DomainExceptionNormalizer (DomainExceptionNormalizer.php:54) only rewrites GraphQL Error objects whose getPrevious() is a DomainException — pre-execution validation errors have no such previous and pass through the default normalizer verbatim; ErrorProvider (ErrorProvider.php:99-101) only substitutes a generic message for GraphQL when status >= 500 (execution-phase exceptions routed through the Error resource), and validation errors never reach it. Thus field/type/mutation names leak via the "Did you mean" oracle despite introspection being disabled. Not in the KNOWN list ([Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-[Security] low: Account-lockout failure counter uses non-atomic read-modify-write (race allows extra guesses) #324 are unrelated). Severity low is appropriate: the oracle only confirms near-miss guesses (Levenshtein-style suggestionList), a weaker leak than full introspection, but it genuinely partially defeats the intended control and matches the OWASP GraphQL "disable field suggestions in production" hardening guidance.
  • Verifier 2 (CONFIRMED, exploitable=true, duplicate=false): Verified against actual code. config/packages/api_platform.yaml:49-53 disables only graphql.introspection.enabled in prod (the DisableIntrospection rule blocks __schema/__type only); graphiql:false disables the UI. There is NO custom error formatter, ErrorHandlerInterface, ExecutorInterface, or ValidationRule decoration anywhere: grep of config/ and src/ for formatError/setErrorFormatter/serializeError/suggestion/ValidationRule/ExecutorInterface returned nothing GraphQL-relevant (only rate-limit resolvers and GraphQLBatchRejectListener). src/Shared/Application/Provider/ErrorProvider.php sanitizes GraphQL runtime 500s (provideInternalServerError returns static 'error.internal') but NOT document-validation errors, which graphql-php produces before resolvers run and returns verbatim in errors[]. composer.lock pins webonyx/graphql-php v15.32.3, whose default FieldsOnCorrectType/KnownTypeNames/KnownArgumentNames rules emit 'Did you mean X?' suggestions with no repo config to disable them. security.yaml:16 makes POST ^/api/graphql PUBLIC_ACCESS and validation runs pre-authorization, so the guess-and-suggest oracle is reachable unauthenticated in prod. This is the residual channel introspection-disable does not cover, distinct from known fixed issues [Security] critical: OAuth2/JWT access tokens silently escalate to ROLE_SERVICE; issuer/audience validation skipped; session-revocation bypassed #312-[Security] low: Account-lockout failure counter uses non-atomic read-modify-write (race allows extra guesses) #324 ([Security] high: GraphQL endpoint bypasses all per-endpoint rate limiters (sign-in, refresh, password-reset, 2FA) #315 covered GraphQL rate limiting, not error disclosure). Mitigating context that lowers impact but does not eliminate the finding: security.yaml:11 exposes ^/api/docs as PUBLIC_ACCESS with no enable_docs:false override, so REST/OpenAPI schema for shared resources is already public; however custom GraphQL mutation resolvers (2FA, refresh-token, sign-out) expose GraphQL-specific field/argument names not fully present in OpenAPI, so the suggestion oracle still leaks genuine non-public naming. Real, currently reachable, low severity, not a duplicate.

Related

Part of the enterprise security-hardening effort — umbrella tracker #348; security NFR issues #426 (securability), #441 (vulnerability), #362 (confidentiality), #389 (integrity). Prior security wave: #312#324. Not a duplicate of the already-fixed items in that range.


Acceptance = the exploit path is closed AND a regression test (unit/Behat/Schemathesis) proves it stays closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendphpPull requests that update Php codesecuritySecurity vulnerability

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions