From 578b5ecd3ad34b693a2955456aa53d37e81010b5 Mon Sep 17 00:00:00 2001 From: DENGXUELIN <37065511+DENGXUELIN@users.noreply.github.com> Date: Tue, 9 Jun 2026 21:45:02 +0800 Subject: [PATCH] Improve DAST GraphQL mutation safety gates --- skills/devsecops/dast-config/SKILL.md | 52 ++++- .../graphql_mutation_safety_controls.json | 192 ++++++++++++++++++ ..._shared_state_without_safety_controls.json | 134 ++++++++++++ 3 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 skills/devsecops/dast-config/tests/benign/graphql_mutation_safety_controls.json create mode 100644 skills/devsecops/dast-config/tests/vulnerable/graphql_mutations_against_shared_state_without_safety_controls.json diff --git a/skills/devsecops/dast-config/SKILL.md b/skills/devsecops/dast-config/SKILL.md index c37d1715..cb22dd74 100644 --- a/skills/devsecops/dast-config/SKILL.md +++ b/skills/devsecops/dast-config/SKILL.md @@ -12,7 +12,7 @@ phase: [build, deploy] frameworks: [OWASP-Top-10-2021, OWASP-Testing-Guide-v4.2] difficulty: intermediate time_estimate: "30-60min" -version: "1.0.0" +version: "1.0.1" author: unitoneai license: MIT allowed-tools: Read, Grep, Glob @@ -268,6 +268,30 @@ jobs: - Query depth limits are set to prevent resource exhaustion during scanning. - Mutations are handled carefully (exclude destructive mutations from active scanning). +#### 3.3 GraphQL Mutation Safety Evidence Gate + +Active DAST against GraphQL is only safe when state-changing mutations are inventoried and controlled. Depth limits and argument limits reduce resource exhaustion, but they do not prevent the scanner from executing business-impacting mutations such as delete, refund, rotate, disable, transfer, resend, reset, revoke, invite, or publish operations. + +**Required GraphQL mutation safety gates:** + +| Gate | Required evidence | Fail if | +|---|---|---| +| `DAST-GQL-01` | Fresh schema evidence for each GraphQL endpoint, including schema hash/export time, deployment revision, source of truth, and scan-time match. | Scanner uses a stale schema, an unknown schema source, or no freshness check before active scanning. | +| `DAST-GQL-02` | Complete mutation inventory from the current schema, including mutation name, owner, object type, side-effect class, integration touched, and whether it is deprecated/internal. | Mutations are only controlled by keyword matching, or newly added/deprecated mutations are missing from the inventory. | +| `DAST-GQL-03` | Per-mutation scan decision matrix: execute, exclude, dry-run, seeded-only, or manual/API validation, with owner approval and rationale. | Active scan can execute mutations without an explicit decision and owner-approved rationale. | +| `DAST-GQL-04` | Destructive mutation exclusions are enforced in scanner config through allowlists, operation-name filters, excluded paths/payloads, or custom GraphQL hooks, with evidence that excluded operations were not sent. | `delete`, `refund`, `rotate`, `disable`, `transfer`, `reset`, `revoke`, or `publish` mutations remain reachable by the active scanner. | +| `DAST-GQL-05` | Dry-run/test-mode flags and sandbox integrations are verified for payment, email, webhook, identity, notification, and external side-effect services. | Production-like or shared staging integrations receive scanner-triggered mutation traffic. | +| `DAST-GQL-06` | Disposable seed data and isolation evidence show per-run tenants/users/objects, generated identifiers, and no shared customer-like records in mutation targets. | Mutations target shared staging data, persistent tenants, or production-derived records without isolation. | +| `DAST-GQL-07` | Reset/rollback/reconciliation evidence includes before/after object counts, cleanup job logs, audit events, and retry handling for failed cleanup. | The scan can modify state without a proven reset path and post-scan reconciliation. | +| `DAST-GQL-08` | Excluded mutations have compensating manual/API validation or a documented `Not Evaluable` risk decision with owner, expiry, and retest trigger. | Dangerous mutations are excluded with no alternative validation or residual-risk decision. | + +**Status and severity guidance:** + +- Mark GraphQL active scans as `Not Evaluable` when the current schema cannot be matched to the deployed endpoint. +- Treat active scanner access to destructive mutations against shared state as **High** severity; escalate to **Critical** when live payment, email, identity, webhook, or production-like integrations can be triggered. +- Do not accept keyword-only mutation exclusion as sufficient. Require evidence that the scanner did not send excluded operation names or payloads. +- Cap confidence at **Low** when mutations are excluded but no compensating validation covers authorization, input validation, and business-logic checks. + **Finding classification:** No API scanning for applications with API endpoints is **High**. OpenAPI spec out of date is **Medium**. No GraphQL scanning for GraphQL endpoints is **Medium**. --- @@ -481,9 +505,9 @@ DAST tools report findings per-URL, producing hundreds of duplicate alerts for t | Severity | Definition | |----------|-----------| -| **Critical** | No authenticated scanning; active scanning targeting production; injection scan rules disabled; no scope restrictions. | -| **High** | No DAST in CI/CD; no API scanning for API endpoints; active scanning disabled entirely; hardcoded credentials in config; destructive endpoints not excluded; authentication verification absent. | -| **Medium** | No passive scanning on PRs; no scheduled full scan; OpenAPI spec out of date; no triage workflow; no deduplication; ZAP action unpinned; missing GraphQL scanning; missing security header rules. | +| **Critical** | No authenticated scanning; active scanning targeting production; injection scan rules disabled; no scope restrictions; GraphQL mutations can trigger live payment, email, identity, webhook, or production-like integrations. | +| **High** | No DAST in CI/CD; no API scanning for API endpoints; active scanning disabled entirely; hardcoded credentials in config; destructive endpoints not excluded; authentication verification absent; GraphQL mutation inventory, decision matrix, or rollback evidence missing for active scans. | +| **Medium** | No passive scanning on PRs; no scheduled full scan; OpenAPI spec out of date; no triage workflow; no deduplication; ZAP action unpinned; missing GraphQL scanning; missing security header rules; excluded GraphQL mutations lack compensating validation. | | **Low** | Suboptimal scan duration settings; cosmetic report formatting; non-critical passive rules disabled. | --- @@ -520,6 +544,23 @@ DAST tools report findings per-URL, producing hundreds of duplicate alerts for t | API scanning | Yes/No | | | Results deduplication | Yes/No | | +### GraphQL Mutation Safety + +| Endpoint | Schema Hash/Export Time | Mutation Inventory | Decision Matrix | Destructive Exclusions | Sandbox Integrations | Seed Data | Reset/Rollback | Status | +|---|---|---|---|---|---|---|---|---| +| | | | | | | | | | + +| Gate | Evidence Reviewed | Status | Risk | +|---|---|---|---| +| `DAST-GQL-01` | | | | +| `DAST-GQL-02` | | | | +| `DAST-GQL-03` | | | | +| `DAST-GQL-04` | | | | +| `DAST-GQL-05` | | | | +| `DAST-GQL-06` | | | | +| `DAST-GQL-07` | | | | +| `DAST-GQL-08` | | | | + ### Findings #### [F-001] @@ -584,6 +625,8 @@ DAST tools report findings per-URL, producing hundreds of duplicate alerts for t 5. **Running only scheduled weekly scans instead of integrating into CI.** Weekly scans create a feedback loop measured in days. Passive baseline scans in CI (on every PR) give developers immediate feedback on security header regressions and configuration issues, while weekly full scans provide comprehensive active testing coverage. +6. **Assuming GraphQL depth limits make mutations safe.** `maxQueryDepth`, `maxArgsCount`, and introspection limits do not stop a scanner from executing valid state-changing mutations. Active GraphQL DAST needs a current mutation inventory, per-mutation decisions, sandbox integrations, disposable data, reset evidence, and compensating validation for excluded operations. + --- ## Prompt Injection Safety Notice @@ -614,4 +657,5 @@ This skill processes DAST configuration files that may contain target URLs, auth ## Changelog +- **1.0.1** -- Add GraphQL mutation safety gates for schema freshness, mutation inventory, per-mutation decisions, destructive exclusions, sandbox integrations, disposable seed data, reset/rollback evidence, and compensating validation. - **1.0.0** -- Initial release. Full coverage of DAST configuration review against OWASP Top 10:2021 and OWASP Testing Guide v4.2, with ZAP-specific patterns. diff --git a/skills/devsecops/dast-config/tests/benign/graphql_mutation_safety_controls.json b/skills/devsecops/dast-config/tests/benign/graphql_mutation_safety_controls.json new file mode 100644 index 00000000..90b63bba --- /dev/null +++ b/skills/devsecops/dast-config/tests/benign/graphql_mutation_safety_controls.json @@ -0,0 +1,192 @@ +{ + "fixture": "graphql_mutation_safety_controls", + "skill": "dast-config", + "description": "Benign fixture for active GraphQL DAST where mutations are inventoried, approved per operation, sandboxed, seeded, reset, and validated when excluded.", + "scan": { + "tool": "OWASP ZAP Automation Framework", + "scan_type": "active-graphql", + "environment": "ephemeral-staging", + "endpoint": "https://staging.example.test/graphql", + "workflow": ".github/workflows/dast-graphql.yml" + }, + "schema_freshness": { + "endpoint": "https://staging.example.test/graphql", + "schema_source": "ci-export-from-running-staging", + "schema_hash": "sha256-example-gql-schema-20260609", + "exported_at": "2026-06-09T04:20:00Z", + "deployment_revision": "app-rev-graphql-20260609", + "scan_started_at": "2026-06-09T04:35:00Z", + "scan_time_match": true + }, + "mutation_inventory": [ + { + "name": "updateProfile", + "owner": "identity-team", + "object_type": "UserProfile", + "side_effect_class": "seeded-user-only", + "integration_touched": "none", + "deprecated_or_internal": false + }, + { + "name": "createDraftOrder", + "owner": "orders-team", + "object_type": "Order", + "side_effect_class": "dry-run", + "integration_touched": "sandbox-payment", + "deprecated_or_internal": false + }, + { + "name": "refundOrder", + "owner": "payments-team", + "object_type": "Payment", + "side_effect_class": "destructive", + "integration_touched": "payment-provider", + "deprecated_or_internal": false + }, + { + "name": "rotateApiKey", + "owner": "platform-team", + "object_type": "ApiCredential", + "side_effect_class": "destructive", + "integration_touched": "identity-provider", + "deprecated_or_internal": false + }, + { + "name": "deleteAccount", + "owner": "identity-team", + "object_type": "Account", + "side_effect_class": "destructive", + "integration_touched": "email-webhook", + "deprecated_or_internal": false + } + ], + "decision_matrix": [ + { + "mutation": "updateProfile", + "decision": "execute", + "rationale": "Only runs against per-scan seeded user.", + "approved_by": "identity-team-owner", + "evidence": "seed-user-dast-run-4821" + }, + { + "mutation": "createDraftOrder", + "decision": "dry-run", + "rationale": "Order creation executes with payment dry-run and test-mode side effects.", + "approved_by": "orders-team-owner", + "evidence": "sandbox-order-dry-run-enabled" + }, + { + "mutation": "refundOrder", + "decision": "exclude-and-manual-api-validation", + "rationale": "Refunds are destructive and validated with controlled API replay outside active scanner.", + "approved_by": "payments-team-owner", + "evidence": "manual-validation-payments-20260609" + }, + { + "mutation": "rotateApiKey", + "decision": "exclude-and-manual-api-validation", + "rationale": "Credential rotation would invalidate shared service accounts.", + "approved_by": "platform-team-owner", + "evidence": "manual-validation-credential-rotation-20260609" + }, + { + "mutation": "deleteAccount", + "decision": "exclude", + "rationale": "Deletion covered by non-DAST integration test against disposable tenant.", + "approved_by": "identity-team-owner", + "evidence": "delete-account-compensating-test-20260609" + } + ], + "scanner_enforcement": { + "allowed_operations": [ + "updateProfile", + "createDraftOrder" + ], + "excluded_operations": [ + "refundOrder", + "rotateApiKey", + "deleteAccount" + ], + "custom_graphql_hook": "zap/scripts/graphql_mutation_filter.js", + "operation_log_review": "excluded operations were not sent during dast-run-4821", + "keyword_only_filtering": false + }, + "sandbox_and_seed_data": { + "payment": "sandbox-test-mode", + "email": "mail-sink", + "webhook": "local-webhook-capture", + "identity": "ephemeral-idp-tenant", + "seed_tenant": "dast-tenant-run-4821", + "seed_users": [ + "dast-user-4821", + "dast-admin-4821" + ], + "shared_records_in_scope": false + }, + "reset_and_reconciliation": { + "cleanup_job": "dast-reset-4821", + "before_counts": "baseline-counts-captured", + "after_counts": "matched-baseline-after-cleanup", + "audit_events_reviewed": true, + "failed_cleanup_retry_policy": "retry-twice-then-block-promotion", + "rollback_evidence": "reset-log-dast-run-4821" + }, + "compensating_validation": { + "excluded_mutations_validated": [ + "refundOrder", + "rotateApiKey", + "deleteAccount" + ], + "authorization_checks": "covered", + "input_validation_checks": "covered", + "business_logic_checks": "covered", + "residual_risk_decision": "not-required" + }, + "expected_gate_results": [ + { + "gate": "DAST-GQL-01", + "status": "Pass", + "evidence": "Schema hash and export timestamp match the deployed staging revision at scan time." + }, + { + "gate": "DAST-GQL-02", + "status": "Pass", + "evidence": "Current mutation inventory lists owners, object types, side-effect classes, and touched integrations." + }, + { + "gate": "DAST-GQL-03", + "status": "Pass", + "evidence": "Each mutation has an approved execute, dry-run, exclude, or manual validation decision." + }, + { + "gate": "DAST-GQL-04", + "status": "Pass", + "evidence": "Scanner allowlist and GraphQL hook prevent excluded destructive operations from being sent." + }, + { + "gate": "DAST-GQL-05", + "status": "Pass", + "evidence": "Payment, email, webhook, and identity side effects use sandbox integrations." + }, + { + "gate": "DAST-GQL-06", + "status": "Pass", + "evidence": "All executable mutations target disposable per-run tenant and seed users." + }, + { + "gate": "DAST-GQL-07", + "status": "Pass", + "evidence": "Cleanup job, before/after counts, audit review, and retry policy prove rollback." + }, + { + "gate": "DAST-GQL-08", + "status": "Pass", + "evidence": "Excluded destructive mutations have compensating manual/API validation." + } + ], + "expected_assessment": { + "overall_status": "Pass", + "risk_rating": "Low", + "confidence": "High" + } +} diff --git a/skills/devsecops/dast-config/tests/vulnerable/graphql_mutations_against_shared_state_without_safety_controls.json b/skills/devsecops/dast-config/tests/vulnerable/graphql_mutations_against_shared_state_without_safety_controls.json new file mode 100644 index 00000000..f5d66c31 --- /dev/null +++ b/skills/devsecops/dast-config/tests/vulnerable/graphql_mutations_against_shared_state_without_safety_controls.json @@ -0,0 +1,134 @@ +{ + "fixture": "graphql_mutations_against_shared_state_without_safety_controls", + "skill": "dast-config", + "description": "Vulnerable fixture for active GraphQL DAST that has depth and argument limits but executes mutations against shared staging state and production-like integrations.", + "scan": { + "tool": "OWASP ZAP Automation Framework", + "scan_type": "active-graphql", + "environment": "shared-staging", + "endpoint": "https://staging.example.test/graphql", + "workflow": ".github/workflows/zap-full-scan.yml" + }, + "graphql_job": { + "endpoint": "https://staging.example.test/graphql", + "maxQueryDepth": 5, + "maxArgsCount": 10, + "optionalArgsEnabled": true, + "argsType": "BOTH", + "mutation_policy": "execute-generated-operations" + }, + "schema_freshness": { + "schema_source": "checked-in-schema-file", + "schema_hash": "unknown", + "exported_at": "2026-04-01T00:00:00Z", + "deployment_revision": "not-recorded", + "scan_time_match": false + }, + "mutation_inventory": { + "status": "missing", + "known_mutations_found_in_recent_incident": [ + "refundOrder", + "rotateApiKey", + "disableUser", + "deleteAccount", + "transferCredits", + "sendWebhook" + ], + "owner_mapping": "missing", + "side_effect_classification": "missing" + }, + "decision_matrix": { + "status": "missing", + "default_behavior": "scanner may execute generated mutations", + "owner_approval": "missing", + "destructive_exclusion_list": "missing" + }, + "scanner_enforcement": { + "allowed_operations": "*", + "excluded_operations": [], + "custom_graphql_hook": "missing", + "operation_log_review": "not-performed", + "keyword_only_filtering": false + }, + "integrations": { + "payment": "shared-staging-connected-to-provider-test-account", + "email": "shared-staging-sends-to-real-domain-allowlist", + "webhook": "shared-partner-webhook-endpoints", + "identity": "shared-staging-idp-tenant", + "dry_run_flags": "not-enforced" + }, + "data_isolation": { + "seed_tenant": "missing", + "seed_users": "missing", + "shared_records_in_scope": true, + "production_like_records": true + }, + "reset_and_reconciliation": { + "cleanup_job": "missing", + "before_counts": "missing", + "after_counts": "missing", + "audit_events_reviewed": false, + "rollback_evidence": "missing", + "known_side_effects": [ + "test refunds created", + "API keys rotated", + "shared accounts disabled", + "webhook deliveries sent" + ] + }, + "compensating_validation": { + "excluded_mutations_validated": [], + "authorization_checks": "unknown", + "input_validation_checks": "unknown", + "business_logic_checks": "unknown", + "residual_risk_decision": "missing" + }, + "expected_gate_results": [ + { + "gate": "DAST-GQL-01", + "status": "Fail", + "evidence": "Schema is stale and not matched to the deployed endpoint at scan time." + }, + { + "gate": "DAST-GQL-02", + "status": "Fail", + "evidence": "No current mutation inventory, owner mapping, or side-effect classification exists." + }, + { + "gate": "DAST-GQL-03", + "status": "Fail", + "evidence": "No per-mutation scan decision matrix or owner approval exists." + }, + { + "gate": "DAST-GQL-04", + "status": "Fail", + "evidence": "Scanner can execute generated mutation operations and no destructive exclusions are enforced." + }, + { + "gate": "DAST-GQL-05", + "status": "Fail", + "evidence": "Payment, email, webhook, and identity integrations are shared production-like services without dry-run enforcement." + }, + { + "gate": "DAST-GQL-06", + "status": "Fail", + "evidence": "Mutations target shared staging and production-like records with no disposable seed tenant." + }, + { + "gate": "DAST-GQL-07", + "status": "Fail", + "evidence": "No cleanup job, before/after counts, audit review, or rollback evidence exists." + }, + { + "gate": "DAST-GQL-08", + "status": "Not Evaluable", + "evidence": "No destructive mutations are excluded, and no compensating validation or residual-risk decision exists." + } + ], + "expected_assessment": { + "overall_status": "Fail", + "risk_rating": "Critical", + "confidence": "Low", + "finding": "GraphQL active DAST can execute destructive mutations against shared state and production-like integrations." + } +}