From a9ca425034bd4575b5fa51fb7791c7512242dbb1 Mon Sep 17 00:00:00 2001 From: Liam Tai-Hogan Date: Thu, 16 Jul 2026 13:42:52 +0100 Subject: [PATCH] v2.4.0: add GraphQL-specific checks (field suggestions + query batching) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New GraphQlCheck (active, PER_HOST, deduped per host+path) adds two GraphQL weaknesses Burp's native scanner does not cover: - Field-suggestion leakage (API9): probes a misspelled meta-field (__typenaem); fires only when the response returns a "Did you mean" hint AND reads as a GraphQL error. Lets an attacker recover the schema even with introspection disabled. Cross-references the native GraphQL introspection / endpoint-found issues. - Array query batching (API4): sends a two-operation JSON array; fires only when the server returns a 2-element result array (both executed). Array batching is not the GraphQL default, so this is a real config signal — an amplification / rate-limit-bypass primitive. Both probes are read-only (__typename), gated by a new HttpUtils.isGraphQlRequest(), reuse the original request's auth headers, and attach the probe as evidence. Endpoint discovery and introspection detection are left to native (no duplication). Docs updated (README, CLAUDE, BURP_DAST_GUIDE, VALIDATION_GUIDE, BAPP_DESCRIPTION): 16 checks / 10 active; GraphQL coverage noted under API4 and API9; also fixed a duplicated Parameter Pollution note in the DAST guide. Version 2.3.2 -> 2.4.0. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/build.yml | 6 +- BAPP_DESCRIPTION.md | 1 + BURP_DAST_GUIDE.md | 24 +- CLAUDE.md | 10 +- README.md | 19 +- VALIDATION_GUIDE.md | 29 +++ pom.xml | 2 +- .../java/com/security/burp/BurpExtender.java | 5 +- .../burp/checks/active/GraphQlCheck.java | 219 ++++++++++++++++++ .../com/security/burp/util/HttpUtils.java | 18 ++ 10 files changed, 303 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/security/burp/checks/active/GraphQlCheck.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46f7ffd..228ae94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -27,13 +27,13 @@ jobs: - name: Confirm fat JAR run: | set -e - test -f target/burp-api-scanner-2.3.2.jar - ls -lh target/burp-api-scanner-2.3.2.jar + test -f target/burp-api-scanner-2.4.0.jar + ls -lh target/burp-api-scanner-2.4.0.jar - name: Upload JAR uses: actions/upload-artifact@v4 with: name: burp-api-scanner-${{ github.sha }} - path: target/burp-api-scanner-2.3.2.jar + path: target/burp-api-scanner-2.4.0.jar if-no-files-found: error retention-days: 30 diff --git a/BAPP_DESCRIPTION.md b/BAPP_DESCRIPTION.md index 38dd0d4..a1bff45 100644 --- a/BAPP_DESCRIPTION.md +++ b/BAPP_DESCRIPTION.md @@ -14,6 +14,7 @@ This extension provides coverage of all ten OWASP API Security Top 10 (2023) cat * All ten OWASP API Security Top 10 (2023) categories in a single extension, with every finding labelled by category so scan output maps straight onto the framework for reporting. * Coverage of the four API-specific risks that native scanning does not address: unrestricted resource consumption, unrestricted access to sensitive business flows, improper inventory management, and unsafe consumption of APIs. * Active discovery of deprecated API versions left reachable in production, by probing earlier versions of any `/vN/` path. +* GraphQL-specific checks that native scanning does not perform: field-suggestion leakage ("Did you mean …", which lets an attacker recover the schema even when introspection is disabled) and array query batching (a rate-limit-bypass and brute-force amplification primitive). * Detection tuned to keep noise down: broken object level authorization fires only when a manipulated identifier returns a materially different object, and parameter pollution measures an endpoint's natural response variation before reporting length-based differences. * Optional AI assistance that filters false positives on noise-prone passive checks and suggests context-specific mass-assignment fields to test. diff --git a/BURP_DAST_GUIDE.md b/BURP_DAST_GUIDE.md index 61b4beb..582b5fa 100644 --- a/BURP_DAST_GUIDE.md +++ b/BURP_DAST_GUIDE.md @@ -16,12 +16,12 @@ The extension is shipped as a single fat JAR. Build with: mvn clean package -DskipTests ``` -Output: `target/burp-api-scanner-2.3.2.jar`. +Output: `target/burp-api-scanner-2.4.0.jar`. In DAST: 1. **Settings → Extensions → Add extension** -2. Upload `burp-api-scanner-2.3.2.jar` +2. Upload `burp-api-scanner-2.4.0.jar` 3. Enable the extension There is no per-DAST configuration — once loaded and enabled, the @@ -35,7 +35,7 @@ startup banner in (that surface only exists in Professional / Community). Verify the extension in DAST like this: 1. Open the scan's **Settings** tab and confirm the extension is listed - under **Extensions** (e.g. `burp-api-scanner-2.3.2.jar`). This proves + under **Extensions** (e.g. `burp-api-scanner-2.4.0.jar`). This proves it loaded and was applied to the scan. 2. After the scan runs, open the **Issues** tab and confirm findings labelled `APIx:2023` are present — this confirms the extension's @@ -53,7 +53,7 @@ extension's Output tab and reads: ``` ==================================== -OWASP API Security Top 10 Scanner v2.3.2 +OWASP API Security Top 10 Scanner v2.4.0 OWASP API Security Top 10 (2023) coverage Edition: Burp Suite Professional AI features: enabled (or "disabled" if Burp AI is off) @@ -62,7 +62,7 @@ AI features: enabled (or "disabled" if Burp AI is off) ## OWASP API Top 10 coverage -Complete OWASP API Security Top 10 (2023) coverage — 15 checks. Several +Complete OWASP API Security Top 10 (2023) coverage — 16 checks. Several checks overlap Burp's native scanner; that overlap is intentional (one extension, all ten categories, OWASP-labelled) and each overlapping issue links to the native check in its detail. Run the native scanner alongside @@ -73,12 +73,12 @@ this extension for the deepest results. | **API1:2023** — Broken Object Level Authorization | Active | Broken access control | | **API2:2023** — Broken Authentication | Active | JWT signature not verified; JWT *none* algorithm; JWT weak HMAC secret; JSON Web Key Set disclosed; Cleartext submission of password | | **API3:2023** — Broken Object Property Level Authorization | Active + Passive | Password returned in later response; Credit card numbers disclosed; Private key disclosed | -| **API4:2023** — Unrestricted Resource Consumption | Passive | — (API-specific) | +| **API4:2023** — Unrestricted Resource Consumption | Active + Passive | — (API-specific; incl. GraphQL query batching) | | **API5:2023** — Broken Function Level Authorization | Active | Broken access control | | **API6:2023** — Unrestricted Access to Sensitive Business Flows | Passive | — (API-specific) | | **API7:2023** — Server-Side Request Forgery | Active | Out-of-band resource load (HTTP); External service interaction; File path traversal | | **API8:2023** — Security Misconfiguration | Active + Passive | CORS; Content security policy; Strict transport security not enforced; Frameable response; Unencrypted communications; Source code disclosure; HTTP TRACE method is enabled | -| **API9:2023** — Improper Inventory Management | Active + Passive | — (API-specific) | +| **API9:2023** — Improper Inventory Management | Active + Passive | GraphQL introspection enabled (for the GraphQL field-suggestion check); otherwise API-specific | | **API10:2023** — Unsafe Consumption of APIs | Active + Passive | — (API-specific) | Each overlapping issue carries a **"Related Burp Scanner checks"** line in @@ -97,12 +97,10 @@ here: if our duplicate marker flips a 200 to a 400, the server is reading the last value and discarding the legitimate first one — a genuine override primitive. Confirm exploitability manually. -**Parameter Pollution note:** the HPP check fires on *any* status change -between the baseline and the polluted request, in either direction -(reported Tentative). A `200 → 400` is **not** treated as safe rejection -here: if our duplicate marker flips a 200 to a 400, the server is reading -the last value and discarding the legitimate first one — a genuine -override primitive. Confirm exploitability manually. +**GraphQL note:** on GraphQL endpoints the extension adds two checks native +scanning does not — field-suggestion leakage ("Did you mean …", API9) and +array query batching (API4). Endpoint discovery and introspection remain +native; the field-suggestion issue cross-references them. ## Burp AI features diff --git a/CLAUDE.md b/CLAUDE.md index b11a2c3..31950f4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This is a Burp Suite extension implementing OWASP API Security Top 10 (2023) coverage. It is built on the Montoya API and targets Burp Suite Professional and Burp Suite DAST. -> **Coverage note.** `main` is the **full build** (15 checks) — complete +> **Coverage note.** `main` is the **full build** (16 checks) — complete > OWASP API Top 10 (2023) coverage in one extension, and this is what we > submit to the BApp Store (renamed **"OWASP API Security Top 10 Scanner"**). > Several checks intentionally overlap Burp's native scanner (injection, @@ -40,9 +40,9 @@ src/main/java/com/security/burp/ │ │ # (BusinessFlow, ExcessiveDataExposure, │ │ # InventoryManagement, ResourceConsumption, │ │ # SecurityMisconfig, UnsafeApiConsumption) -│ └── active/ # 9 active checks, all extend AbstractActiveCheck +│ └── active/ # 10 active checks, all extend AbstractActiveCheck │ └── injection/ # (BrokenObjectAuth, BrokenAuth, DeprecatedVersionProbe, -│ # FunctionLevelAuth, Injection, MassAssignment, +│ # FunctionLevelAuth, GraphQl, Injection, MassAssignment, │ # MethodFuzzing, ParameterPollution, Ssrf). │ # InjectionCheck splits into injection/{AuthBypassTester, │ # InjectionPayloads}. @@ -66,13 +66,13 @@ export PATH="$JAVA_HOME/bin:$PATH" mvn clean package -DskipTests ``` -Output: `target/burp-api-scanner-2.3.2.jar` (~370 KB fat JAR). +Output: `target/burp-api-scanner-2.4.0.jar` (~370 KB fat JAR). Load in Burp via **Extensions → Installed → Add → Java**. ## Conventions -These are the patterns established across all 15 checks. Stick to them +These are the patterns established across all 16 checks. Stick to them when adding new ones — Hannah's review feedback was the catalyst for the v2 rewrite, and breaking these breaks the property she cared about (reviewable code). diff --git a/README.md b/README.md index d6ee7ab..e86c2ad 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ registered only under Professional. mvn clean package -DskipTests ``` -Produces `target/burp-api-scanner-2.3.2.jar`. Load via **Extensions → +Produces `target/burp-api-scanner-2.4.0.jar`. Load via **Extensions → Installed → Add → Java**. Requires JDK 17+ (Montoya API requirement) and Maven 3.6+. @@ -45,7 +45,7 @@ The banner in the extension's Output tab will look like: ``` ==================================== -OWASP API Security Top 10 Scanner v2.3.2 +OWASP API Security Top 10 Scanner v2.4.0 OWASP API Security Top 10 (2023) coverage Edition: Burp Suite Professional AI features: enabled @@ -54,19 +54,19 @@ AI features: enabled ## Coverage -Full OWASP API Security Top 10 (2023), 15 checks: +Full OWASP API Security Top 10 (2023), 16 checks: | OWASP category | Detection | Related native check(s) | |---|---|---| | API1:2023 — Broken Object Level Authorization | Active | Broken access control | | API2:2023 — Broken Authentication | Active | JWT signature not verified · JWT none algorithm · JWT weak HMAC secret · JSON Web Key Set disclosed · Cleartext submission of password | | API3:2023 — Broken Object Property Level Authorization | Active + passive | Password returned in later response · Credit card numbers disclosed · Private key disclosed | -| API4:2023 — Unrestricted Resource Consumption | Passive | — (API-specific) | +| API4:2023 — Unrestricted Resource Consumption | Active + passive | — (API-specific; incl. GraphQL query batching) | | API5:2023 — Broken Function Level Authorization | Active | Broken access control | | API6:2023 — Unrestricted Access to Sensitive Business Flows | Passive | — (API-specific) | | API7:2023 — Server-Side Request Forgery | Active | Out-of-band resource load (HTTP) · External service interaction · File path traversal | | API8:2023 — Security Misconfiguration | Active + passive | CORS · Content security policy · Strict transport security not enforced · Frameable response · Unencrypted communications · Source code disclosure · HTTP TRACE method is enabled | -| API9:2023 — Improper Inventory Management | Active + passive | — (API-specific) | +| API9:2023 — Improper Inventory Management | Active + passive | GraphQL introspection enabled (for the GraphQL field-suggestion check) | | API10:2023 — Unsafe Consumption of APIs | Active + passive | — (API-specific) | Every issue in the overlapping categories links to its native @@ -86,6 +86,11 @@ equivalent — they are the unique coverage this extension adds. - **HTTP Parameter Pollution** — fires on any response change (in either direction) when a parameter is duplicated; a marker-induced `200 → 400` is treated as a real override primitive, not safe rejection. +- **GraphQL checks native scanning skips** — flags field-suggestion + leakage ("Did you mean …", which lets an attacker recover the schema + even with introspection off) and array query batching (a rate-limit + bypass / brute-force amplification primitive). Endpoint discovery and + introspection stay native and are cross-referenced. - **Burp AI integration** (optional) — when `api.ai().isEnabled()` is true, two extra features activate automatically: - **Passive triage**: false-positive filter on noise-prone passive @@ -118,14 +123,14 @@ com.security.burp/ │ ├── AbstractPassiveCheck # base classes — exception handling, triage │ ├── AbstractActiveCheck │ ├── passive/ # 6 passive checks -│ └── active/ # 9 active checks +│ └── active/ # 10 active checks │ └── injection/ # InjectionCheck helpers (payloads, auth-bypass tester) ├── scanner/EndpointRegistry # shared state for the UI tab ├── ui/ScannerTab # Swing tab listing discovered endpoints └── util/IssueBuilder # fluent AuditIssue construction ``` -15 scan checks total, registered individually with the appropriate +16 scan checks total, registered individually with the appropriate `ScanCheckType` (`PER_INSERTION_POINT`, `PER_HOST`, or `PER_REQUEST`). See [CLAUDE.md](CLAUDE.md) for the full breakdown. diff --git a/VALIDATION_GUIDE.md b/VALIDATION_GUIDE.md index d16a110..80d45b5 100644 --- a/VALIDATION_GUIDE.md +++ b/VALIDATION_GUIDE.md @@ -201,6 +201,35 @@ deprecated on its own — it is the current version on most APIs.) - Check whether the old version misses security fixes documented for the new version. +### API9:2023 — GraphQL field suggestions enabled + +**Finding:** a query for the misspelled meta-field `__typenaem` returned +a "Did you mean …" hint, so the server leaks field names on unknown-field +errors — schema recovery is possible even with introspection disabled. + +**Questions to answer:** Is introspection also on (then this is moot — +the schema is already public)? If introspection is off, this defeats that +control. + +**How to validate:** in Repeater, send `{ __typenaem }` and confirm the +suggestion; then query another misspelled field name and observe the +server hinting real field names. Cross-check Burp's native *GraphQL +introspection enabled* issue for the same endpoint. + +### API4:2023 — GraphQL query batching enabled + +**Finding:** a JSON array of two operations was accepted and both were +executed (a two-element result array returned). + +**Questions to answer:** Does the endpoint expose a sensitive operation +(login, OTP, password reset) that batching/aliasing could amplify past a +per-request rate limit? + +**How to validate:** in Repeater, batch (or alias) a sensitive mutation N +times in one request and check whether all N attempts are processed and +whether rate limiting counts operations or only HTTP requests. Batching +alone is only impactful when paired with such an operation. + --- ## TENTATIVE — heuristic, high false-positive rate diff --git a/pom.xml b/pom.xml index 10cb265..277bc75 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.security burp-api-scanner - 2.3.2 + 2.4.0 jar OWASP API Security Top 10 Scanner diff --git a/src/main/java/com/security/burp/BurpExtender.java b/src/main/java/com/security/burp/BurpExtender.java index 529696b..1115390 100644 --- a/src/main/java/com/security/burp/BurpExtender.java +++ b/src/main/java/com/security/burp/BurpExtender.java @@ -12,6 +12,7 @@ import com.security.burp.checks.active.BrokenObjectAuthCheck; import com.security.burp.checks.active.DeprecatedVersionProbeCheck; import com.security.burp.checks.active.FunctionLevelAuthCheck; +import com.security.burp.checks.active.GraphQlCheck; import com.security.burp.checks.active.InjectionCheck; import com.security.burp.checks.active.MassAssignmentCheck; import com.security.burp.checks.active.MethodFuzzingCheck; @@ -115,6 +116,8 @@ private void registerScanChecks(MontoyaApi api, new BrokenAuthCheck(api), ScanCheckType.PER_HOST); api.scanner().registerActiveScanCheck( new DeprecatedVersionProbeCheck(api), ScanCheckType.PER_HOST); + api.scanner().registerActiveScanCheck( + new GraphQlCheck(api), ScanCheckType.PER_HOST); // Passive checks. PER_REQUEST runs once per HTTP transaction. AiTriage // filters out contextual false positives for each before they surface. @@ -164,7 +167,7 @@ private void registerUnloadingHandler(MontoyaApi api, private void logBanner(MontoyaApi api, BurpSuiteEdition edition, boolean aiAvailable) { api.logging().logToOutput("===================================="); - api.logging().logToOutput(EXTENSION_NAME + " v2.3.2"); + api.logging().logToOutput(EXTENSION_NAME + " v2.4.0"); api.logging().logToOutput("OWASP API Security Top 10 (2023) coverage"); api.logging().logToOutput("Edition: " + edition.displayName()); api.logging().logToOutput("AI features: " + (aiAvailable ? "enabled" : "disabled")); diff --git a/src/main/java/com/security/burp/checks/active/GraphQlCheck.java b/src/main/java/com/security/burp/checks/active/GraphQlCheck.java new file mode 100644 index 0000000..430ac03 --- /dev/null +++ b/src/main/java/com/security/burp/checks/active/GraphQlCheck.java @@ -0,0 +1,219 @@ +package com.security.burp.checks.active; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.http.Http; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; +import burp.api.montoya.scanner.audit.issues.AuditIssue; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.security.burp.checks.AbstractActiveCheck; +import com.security.burp.util.HttpUtils; +import com.security.burp.util.IssueBuilder; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * GraphQL-specific weaknesses that Burp's native scanner does not check. + * + *

Native coverage already reports GraphQL endpoint discovery and + * introspection; this check adds two things it does not: + *

    + *
  1. Field-suggestion leakage (API9) — even with introspection + * disabled, many servers answer an unknown-field query with a + * "Did you mean …" hint, letting an attacker recover the schema field + * by field;
  2. + *
  3. Array query batching (API4) — accepting a JSON array of + * operations in one request enables amplification and per-request + * rate-limit bypass (batched/aliased brute force).
  4. + *
+ * + *

Registered {@code PER_HOST}; deduped per (host + path). Both probes are + * read-only — they query {@code __typename} (or a misspelled variant), + * so they never mutate server state. + */ +public final class GraphQlCheck extends AbstractActiveCheck { + + /** Misspelled meta-field: a server with suggestions replies "Did you mean __typename". */ + private static final String SUGGESTION_PROBE = "{\"query\":\"{ __typenaem }\"}"; + + /** Two-operation array batch; a server that runs both returns a 2-element array. */ + private static final String BATCH_PROBE = + "[{\"query\":\"{ __typename }\"},{\"query\":\"{ __typename }\"}]"; + + private static final String BACKGROUND_API9 = + "API9:2023 - Improper Inventory Management

" + + "GraphQL serves a whole typed schema from one endpoint. When the server leaks " + + "schema detail, an attacker can map the full API surface — including fields never " + + "meant to be public — without any documentation."; + + private static final String BACKGROUND_API4 = + "API4:2023 - Unrestricted Resource Consumption

" + + "GraphQL lets a client request many operations in a single call. Without limits " + + "this multiplies server work per request and can be used to bypass controls that " + + "count HTTP requests rather than operations."; + + /** Cross-reference to Burp's native GraphQL scan checks. */ + private static final String RELATED_INTROSPECTION = + "

Related Burp Scanner checks: for further detail refer to the native " + + "GraphQL introspection enabled and " + + "GraphQL endpoint found checks."; + + private final Set dedupe = Collections.newSetFromMap(new ConcurrentHashMap<>()); + + public GraphQlCheck(MontoyaApi api) { + super(api); + } + + @Override + public String checkName() { + return "GraphQL weaknesses (field suggestions, batching)"; + } + + @Override + protected List audit(HttpRequestResponse rr, AuditInsertionPoint ip, Http http) { + if (!shouldRunOnce(rr)) return List.of(); + if (!HttpUtils.isGraphQlRequest(rr.request())) return List.of(); + + List issues = new ArrayList<>(); + AuditIssue suggestions = trySuggestions(rr, http); + if (suggestions != null) issues.add(suggestions); + AuditIssue batching = tryBatching(rr, http); + if (batching != null) issues.add(batching); + return issues; + } + + private boolean shouldRunOnce(HttpRequestResponse rr) { + String key = rr.request().httpService().host() + "|" + rr.request().pathWithoutQuery(); + return dedupe.add(key); + } + + // ---- Probes ------------------------------------------------------------- + + /** + * Fire only when the server returns a GraphQL field suggestion for a + * deliberately-misspelled meta-field. The response literally naming a + * "Did you mean" hint is the leak; requiring the reply to read as + * a GraphQL error as well keeps a generic "did you mean" from some other + * handler out. + */ + private AuditIssue trySuggestions(HttpRequestResponse rr, Http http) { + HttpRequestResponse evidence = sendGraphQl(rr, http, SUGGESTION_PROBE); + if (evidence == null) return null; + String body = evidence.response().bodyToString(); + if (body == null) return null; + String lower = body.toLowerCase(Locale.ROOT); + boolean leaks = lower.contains("did you mean") + && (lower.contains("cannot query field") || lower.contains("\"errors\"")); + return leaks ? buildSuggestionsIssue(rr, evidence) : null; + } + + /** + * Fire only when the server executes a two-operation JSON array batch and + * returns a matching 2-element array of results — proof array batching is + * enabled. This is not the GraphQL default; many servers reject an array + * body, so a genuine 2-element result array is a real configuration signal. + */ + private AuditIssue tryBatching(HttpRequestResponse rr, Http http) { + HttpRequestResponse evidence = sendGraphQl(rr, http, BATCH_PROBE); + if (evidence == null) return null; + return batchExecuted(evidence.response().bodyToString()) + ? buildBatchingIssue(rr, evidence) + : null; + } + + private static boolean batchExecuted(String body) { + if (body == null) return false; + try { + JsonElement root = JsonParser.parseString(body); + if (!root.isJsonArray()) return false; + JsonArray array = root.getAsJsonArray(); + // Both operations must have been processed — each element a GraphQL + // result object (carrying data or errors). + return array.size() >= 2 && isGraphQlResult(array.get(0)) && isGraphQlResult(array.get(1)); + } catch (JsonSyntaxException e) { + return false; + } + } + + private static boolean isGraphQlResult(JsonElement element) { + if (!element.isJsonObject()) return false; + JsonObject object = element.getAsJsonObject(); + return object.has("data") || object.has("errors"); + } + + // ---- HTTP --------------------------------------------------------------- + + private HttpRequestResponse sendGraphQl(HttpRequestResponse rr, Http http, String jsonBody) { + try { + HttpRequest probe = rr.request().withMethod("POST").withBody(jsonBody); + // Reuse the original request (keeps auth cookies/headers). Only add a + // JSON Content-Type when it isn't already JSON, to avoid a duplicate + // header on the common application/json GraphQL request. + if (!HttpUtils.isJson(probe)) { + probe = probe.withAddedHeader("Content-Type", "application/json"); + } + HttpRequestResponse response = http.sendRequest(probe); + return (response != null && response.hasResponse()) ? response : null; + } catch (Exception e) { + api.logging().logToError("[GraphQL] probe send failed: " + e.getMessage()); + return null; + } + } + + // ---- Issues ------------------------------------------------------------- + + private AuditIssue buildSuggestionsIssue(HttpRequestResponse base, HttpRequestResponse evidence) { + String detail = + "The GraphQL endpoint returns field suggestions when a query references an " + + "unknown field. Sending the misspelled meta-field __typenaem produced " + + "a \"Did you mean …\" hint in the response.

" + + "Even with introspection disabled, an attacker can recover the schema field by " + + "field by iterating unknown-field errors and reading the suggestions — defeating " + + "the point of hiding it." + + RELATED_INTROSPECTION; + return IssueBuilder.issue(base) + .name("API9:2023 - Improper Inventory Management (GraphQL Field Suggestions Enabled)") + .detail(detail) + .remediation("Strip field suggestions from error responses in production — e.g. a " + + "custom validation rule or error formatter that removes \"Did you mean\" " + + "hints — the same way you disable introspection.") + .background(BACKGROUND_API9) + .severity("Low") + .confidence("Firm") + .evidence(base, evidence) + .build(); + } + + private AuditIssue buildBatchingIssue(HttpRequestResponse base, HttpRequestResponse evidence) { + String detail = + "The GraphQL endpoint accepts array-batched requests: a JSON array of two " + + "operations was sent in one request and the server executed both, returning a " + + "two-element result array.

" + + "Batching — and field aliasing — let a client pack many operations into a single " + + "request. Combined with a sensitive operation such as login or OTP verification, " + + "this multiplies attempts per request and can bypass rate limiting that counts " + + "HTTP requests (a brute-force amplification primitive). Whether it is exploitable " + + "depends on what operations the endpoint exposes."; + return IssueBuilder.issue(base) + .name("API4:2023 - Unrestricted Resource Consumption (GraphQL Query Batching Enabled)") + .detail(detail) + .remediation("Disable array-based batching if it is not needed, cap the number of " + + "operations and aliases per request, and rate-limit on operations rather " + + "than on HTTP requests alone.") + .background(BACKGROUND_API4) + .severity("Low") + .confidence("Firm") + .evidence(base, evidence) + .build(); + } +} diff --git a/src/main/java/com/security/burp/util/HttpUtils.java b/src/main/java/com/security/burp/util/HttpUtils.java index e6ada55..e3c6fcb 100644 --- a/src/main/java/com/security/burp/util/HttpUtils.java +++ b/src/main/java/com/security/burp/util/HttpUtils.java @@ -102,6 +102,24 @@ public static boolean isModifyingMethod(String method) { return "POST".equals(method) || "PUT".equals(method) || "PATCH".equals(method); } + /** + * True if the request targets a GraphQL endpoint — either the path + * contains {@code graphql}, or it is a JSON request whose body carries a + * GraphQL operation (a {@code "query"} field wrapping a selection set). + * Used to gate the GraphQL-specific active checks so they only probe real + * GraphQL endpoints. + */ + public static boolean isGraphQlRequest(HttpRequest request) { + if (request == null) return false; + String path = request.pathWithoutQuery(); + if (path != null && path.toLowerCase(Locale.ROOT).contains("graphql")) return true; + if (isJson(request)) { + String body = request.bodyToString(); + return body != null && body.contains("\"query\"") && body.contains("{"); + } + return false; + } + private static boolean contentTypeContains(HttpRequest request, String needle) { for (HttpHeader header : request.headers()) { if ("content-type".equalsIgnoreCase(header.name())) {