This document describes the security measures implemented in the SOTI MobiControl GraphQL Wrapper to protect against common attack vectors.
- Overview
- GraphQL Security
- Input Validation
- Injection Prevention
- HTTP Security Headers
- CORS Configuration
- Logging
- Configuration Reference
- Security Checklist
This GraphQL wrapper implements a "Defense in Depth" security architecture with protections at multiple layers:
| Layer | Protection |
|---|---|
| Network | CORS, Security Headers |
| GraphQL | Introspection control, Depth limiting, Cost analysis |
| Input | Pydantic validation, URL encoding |
| Output | Error masking, Logging sanitization |
What it does: Disables GraphQL schema introspection in production environments.
Why it matters: Introspection allows clients to query the entire schema structure (__schema, __type). Attackers can use this to understand the API surface and find potential vulnerabilities.
Implementation: app/security/extensions.py - DisableIntrospection
# Introspection queries like this are blocked in production:
query {
__schema {
types { name }
}
}Configuration: Automatically enabled when ENVIRONMENT=prod.
What it does: Limits the nesting depth of GraphQL queries to 6 levels.
Why it matters: Deeply nested queries can cause exponential resource consumption, leading to denial of service.
Implementation: Uses Strawberry's built-in QueryDepthLimiter.
# This query would be rejected (too deep):
query {
devices { # Level 1
items { # Level 2
customAttributes { # Level 3
name # Level 4
nested { # Level 5
field { # Level 6
too { # Level 7 - REJECTED
deep
}
}
}
}
}
}
}Error Response:
{
"errors": [{
"message": "Query is too deep (max depth: 6)"
}]
}What it does: Calculates a "cost" for each query and rejects queries that exceed the maximum allowed cost (1000).
Why it matters: Prevents resource exhaustion attacks where attackers craft expensive queries that consume excessive server resources.
Implementation: app/security/extensions.py - QueryCostLimiter
Cost Factors:
- Number of fields requested
- Query depth
- List fields (multiplied by 2)
What it does: Hides internal error details in production, showing only safe error messages to clients.
Why it matters: Stack traces and internal error messages can reveal sensitive information about the system architecture, file paths, and potential vulnerabilities.
Implementation: app/security/extensions.py - ErrorMaskingExtension
Safe errors (shown to clients):
- Input validation errors (
ValueError) - Permission errors (
PermissionError) - Introspection blocked messages
- Query complexity errors
Masked errors (hidden in production):
- Database errors
- Internal exceptions
- Stack traces
Example:
Development response:
{
"errors": [{
"message": "ConnectionRefusedError: [Errno 111] Connection refused",
"path": ["device"],
"extensions": {
"exception": "..."
}
}]
}Production response:
{
"errors": [{
"message": "Internal server error"
}]
}All user inputs are validated using Pydantic before processing.
Location: app/security/validation.py
# Valid: alphanumeric, hyphens, underscores, periods
"device-123"
"abc_def.456"
# Invalid: path traversal, special characters
"../../../etc/passwd" # REJECTED
"device;DROP TABLE" # REJECTED# Valid: includes backslashes for Windows-style paths
"\\Root\\MyGroup"
# Invalid: path traversal
"..\\..\\secret" # REJECTED# Valid ranges:
skip: 0 - 10000
take: 1 - 100
# Invalid values are clamped:
take: 500 -> 100 (max)
skip: -1 -> 0 (min)All path parameters are URL-encoded before being sent to the MobiControl REST API.
Location: app/client/mobicontrol.py
Before (vulnerable):
# Attacker could inject: device_id = "../../../admin"
f"/devices/{device_id}"After (secure):
# Path traversal is encoded: %2E%2E%2F%2E%2E%2F%2E%2E%2Fadmin
f"/devices/{quote(device_id, safe='')}"The following security headers are added to all responses.
Location: app/main.py - SecurityHeadersMiddleware
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options |
nosniff |
Prevents MIME type sniffing |
X-Frame-Options |
DENY |
Prevents clickjacking |
X-XSS-Protection |
1; mode=block |
Enables XSS filter |
Referrer-Policy |
strict-origin-when-cross-origin |
Controls referrer info |
Strict-Transport-Security |
max-age=31536000; includeSubDomains |
Enforces HTTPS (prod only) |
Content-Security-Policy |
default-src 'self'; script-src 'none' |
Restricts resource loading (prod only) |
Cross-Origin Resource Sharing is configured based on environment.
Permissive CORS for easier development:
allow_origins=["*"]
allow_methods=["*"]
allow_headers=["*"]Strict CORS with allowlist:
allow_origins=settings.cors_origins # From config
allow_methods=["GET", "POST", "OPTIONS"]
allow_headers=["Authorization", "Content-Type"]Set allowed origins in .env:
CORS_ORIGINS=["https://your-frontend.com","https://admin.your-domain.com"]| Environment | Log Level | Details |
|---|---|---|
dev |
DEBUG | Full debug information |
test |
INFO | Standard operational logs |
prod |
INFO | Minimal, no sensitive data |
- GraphQL query errors (full details logged internally, masked externally)
- Security events (introspection attempts, rejected queries)
- Authentication events (token refresh, failures)
- Passwords or secrets (masked with
***) - Full tokens (only first 8 characters shown)
- User PII
| Variable | Default | Description |
|---|---|---|
ENVIRONMENT |
dev |
Environment: dev, test, prod |
DEBUG |
false |
Enable debug mode (dev only) |
CORS_ORIGINS |
["http://localhost:3000"] |
Allowed CORS origins (JSON array) |
| Setting | Value | Configurable |
|---|---|---|
| Max query depth | 6 | No (code change required) |
| Max query cost | 1000 | No (code change required) |
Max pagination take |
100 | No (hardcoded) |
Max pagination skip |
10000 | No (hardcoded) |
Before deploying to production, verify:
-
ENVIRONMENT=prodis set -
DEBUG=falseis set -
CORS_ORIGINScontains only trusted domains - GraphQL introspection returns error
- Deep queries (>6 levels) are rejected
- Security headers are present in responses
- Error responses don't contain stack traces
- Logs don't contain sensitive data
# Test introspection is disabled (should fail in prod)
curl -X POST http://localhost:8000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { types { name } } }"}'
# Test deep query rejection
curl -X POST http://localhost:8000/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ devices { items { customAttributes { name } } } }"}'
# Verify security headers
curl -I http://localhost:8000/healthIf you discover a security vulnerability, please report it responsibly:
- Do not open a public GitHub issue
- Email the maintainers directly with details
- Allow reasonable time for a fix before disclosure