-
Notifications
You must be signed in to change notification settings - Fork 615
[TESTING][SECURITY]: API security manual test plan (mass assignment, BOLA, parameter pollution, OpenAPI validation) #2412
Description
[TESTING][SECURITY]: API security manual test plan (mass assignment, BOLA, parameter pollution, OpenAPI validation)
Goal
Produce a comprehensive manual test plan for validating API-specific security controls including mass assignment protection, Broken Object Level Authorization (BOLA) prevention, HTTP parameter pollution handling, and OpenAPI schema validation.
Why Now?
Security testing is critical for GA release:
- Production Readiness: Security must be validated before release
- Compliance: Required by security standards and audits
- Defense in Depth: Validates multiple protection layers
- Attack Mitigation: Prevents common exploitation techniques
- User Trust: Security issues erode confidence
Test Environment Setup
# Build and start
make docker-prod && make compose-down && make testing-up
# Key settings in .env:
AUTH_REQUIRED=true
MCPGATEWAY_ADMIN_API_ENABLED=true
# NOTE: There is NO RBAC_ENABLED setting
# Create tokens (use --teams flag, NOT --team-id)
export JWT_SECRET_KEY="${JWT_SECRET_KEY:-test-secret-32-chars-minimum!!}"
export ADMIN_TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username admin@example.com --exp 10080 --secret "$JWT_SECRET_KEY" --admin)
export TOKEN=$(python -m mcpgateway.utils.create_jwt_token \
--username user@example.com --teams team-alpha --exp 10080 --secret "$JWT_SECRET_KEY")User Stories
Story 1: Mass Assignment Protection
As a security administrator
I want mass assignment attacks prevented
So that users cannot modify protected fields via API
Acceptance Criteria
Feature: Mass assignment protection
Scenario: Protected fields controlled by server
When a user submits a request with "is_admin": true
Then the is_admin field may be accepted but controlled by permissions
And only admin users can set is_admin=true
Scenario: System fields protected
When a user updates a resource
Then fields like created_by are set server-side
And cannot be overwritten by the callerNote: Fields like team_id and is_admin ARE in schemas but are validated/controlled server-side. Services fall back to schema team_id when endpoint doesn't provide one.
Story 2: BOLA Prevention
As a security administrator
I want Broken Object Level Authorization prevented
So that users cannot access other users' resources by ID manipulation
Acceptance Criteria
Feature: BOLA prevention (IDOR)
Scenario: User cannot access another user's resource
When User A requests User B's private resource by ID
Then the request should return 403 Forbidden
And the resource content should not be exposed
Scenario: Sequential ID enumeration prevented
When a user tries to enumerate IDs sequentially
Then only their own resources should be returnedStory 3: HTTP Parameter Pollution
As a security administrator
I want HTTP Parameter Pollution handled correctly
So that duplicate parameters don't bypass security controls
Acceptance Criteria
Feature: HTTP Parameter Pollution
Scenario: Duplicate query parameters
When a request contains duplicate parameter names
Then the server uses the LAST value (Starlette behavior)
And security checks should apply correctly
Scenario: Mixed source parameters
When parameters come from query, body, and path
Then body takes precedence over query
And no security bypass possibleStory 4: OpenAPI Validation
As a security administrator
I want requests validated against OpenAPI schema
So that malformed requests are rejected
Acceptance Criteria
Feature: OpenAPI validation
Scenario: Invalid request body rejected
When a request body doesn't match schema
Then the request should be rejected with 400/422
And validation error should be returned
Scenario: Extra fields handled
When a request contains fields not in schema
Then the extra fields are ignored (Pydantic default)Architecture
+---------------------------------------------------------------------+
| API Security Layer |
| |
| +-------------------------------------------------------------+ |
| | Mass Assignment Protection | |
| | | |
| | Pydantic Models (schemas.py): | |
| | - team_id IS in ToolCreate (line 323), ResourceCreate | |
| | - is_admin IS in EmailRegistrationRequest (line 5204) | |
| | - created_by is set SERVER-SIDE, not from request | |
| | - Services FALL BACK to schema team_id when not provided | |
| +-------------------------------------------------------------+ |
| |
| +-------------------------------------------------------------+ |
| | BOLA Prevention | |
| | | |
| | 1. Authenticate request (get user identity) | |
| | 2. Fetch requested resource | |
| | 3. Check ownership/permissions via RBAC middleware | |
| | - team_id matching | |
| | - OR current_user has explicit permission | |
| | - OR resource visibility is public | |
| | 4. Return 403 if check fails | |
| +-------------------------------------------------------------+ |
| |
| +-------------------------------------------------------------+ |
| | API Endpoints | |
| | | |
| | JSON APIs: /tools, /resources, /gateways | |
| | Form APIs: /admin/tools, /admin/resources (form data) | |
| | Form Edit: /admin/tools/{id}/edit (POST form update) | |
| | /admin/resources/{id}/edit (POST form update) | |
| | User Admin: /auth/email/admin/users/{user_email} | |
| +-------------------------------------------------------------+ |
+---------------------------------------------------------------------+
Test Cases
TC-API-001: Mass Assignment - Tool Creation (JSON API)
Objective: Verify protected fields are controlled server-side
Steps:
# Use /tools for JSON API (NOT /admin/tools which expects form data)
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"test-tool","description":"Test","created_by":"attacker@evil.com","visibility":"public"}' \
http://localhost:4444/tools | jq .
# Verify created_by was NOT set to attacker value (set server-side)Expected Results:
- created_by field ignored (set server-side)
- Tool created successfully
- No error (field silently ignored)
TC-API-002: Mass Assignment - Admin User Creation
Objective: Verify is_admin is controlled by permissions
Steps:
# NOTE: is_admin IS in EmailRegistrationRequest schema (schemas.py:5204)
# But only admins should be able to set is_admin=true
# Step 1: Non-admin tries to create admin user
curl -s -w "\nStatus: %{http_code}\n" \
-X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"newuser@example.com","password":"SecurePass123!","is_admin":true}' \
http://localhost:4444/auth/email/admin/users
# Step 2: Admin creates admin user
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email":"newadmin@example.com","password":"SecurePass123!","is_admin":true}' \
http://localhost:4444/auth/email/admin/users | jq .Expected Results:
- Non-admin: Request denied (403)
- Admin: User created with is_admin=true
TC-API-003: BOLA - Cross-Team Access
Objective: Verify users cannot access other teams' resources
Steps:
# User A (Team Alpha) - use --teams flag, NOT --team-id
TOKEN_A=$(python -m mcpgateway.utils.create_jwt_token \
--username usera@example.com --teams alpha --exp 60 --secret "$JWT_SECRET_KEY")
# User B (Team Beta)
TOKEN_B=$(python -m mcpgateway.utils.create_jwt_token \
--username userb@example.com --teams beta --exp 60 --secret "$JWT_SECRET_KEY")
# Create tool with User A
TOOL_ID=$(curl -s -X POST -H "Authorization: Bearer $TOKEN_A" \
-H "Content-Type: application/json" \
-d '{"name":"team-tool","visibility":"team"}' \
http://localhost:4444/tools | jq -r '.id')
# Try to access with User B - should fail or return empty
curl -s -w "\nStatus: %{http_code}\n" \
-H "Authorization: Bearer $TOKEN_B" \
http://localhost:4444/tools/$TOOL_IDExpected Results:
- User B gets 403 Forbidden or 404
- Resource content not exposed
- No information leakage
TC-API-004: BOLA - Resource Modification
Objective: Verify users cannot modify others' resources
Steps:
# Step 1: User B tries to update User A's resource
curl -s -w "\nStatus: %{http_code}\n" \
-X PUT -H "Authorization: Bearer $TOKEN_B" \
-H "Content-Type: application/json" \
-d '{"name":"Hacked Resource"}' \
http://localhost:4444/tools/$USER_A_TOOL_ID
# Step 2: User B tries to delete User A's resource
curl -s -w "\nStatus: %{http_code}\n" \
-X DELETE -H "Authorization: Bearer $TOKEN_B" \
http://localhost:4444/tools/$USER_A_TOOL_IDExpected Results:
- Update returns 403 Forbidden
- Delete returns 403 Forbidden
- Resource unchanged
TC-API-005: HTTP Parameter Pollution - Query
Objective: Verify duplicate query parameters handled safely
Steps:
# Starlette uses LAST value for duplicate params (NOT first)
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:4444/tools?limit=10&limit=100" | jq 'length'
# Result will use limit=100 (LAST value)Expected Results:
- LAST value used (Starlette behavior)
- Consistent handling
- No security bypass possible
TC-API-006: HTTP Parameter Pollution - Body vs Query
Objective: Verify body parameters take precedence over query
Steps:
# Conflicting query and body params
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Body Name"}' \
"http://localhost:4444/tools?name=Query+Name" | jq .nameExpected Results:
- Body value used ("Body Name")
- Clear precedence (body over query)
- No confusion in security checks
TC-API-007: User Admin Endpoint
Objective: Verify correct user admin endpoint
Steps:
# Get user by EMAIL (not ID) - correct endpoint path
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:4444/auth/email/admin/users/testuser@example.com | jq .Expected Results:
- Endpoint uses email in path (not ID)
- Correct path: /auth/email/admin/users/{user_email}
TC-API-008: Form vs JSON Endpoints
Objective: Verify /admin/* endpoints expect form data
Steps:
# /admin/tools expects FORM data, not JSON
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-F "name=form-tool" \
-F "description=Created via form" \
http://localhost:4444/admin/tools
# JSON API is at /tools
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"json-tool","description":"Created via JSON"}' \
http://localhost:4444/tools | jq .Expected Results:
- /admin/tools: Accepts form data
- /tools: Accepts JSON
TC-API-009: Form Update Endpoints
Objective: Verify form update routes have /edit suffix
Steps:
# Form UPDATE routes have /edit suffix
# /admin/tools/{id} is GET (view JSON)
# /admin/tools/{id}/edit is POST (form update)
# Get tool as JSON (GET)
curl -s -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:4444/admin/tools/$TOOL_ID | jq .
# Update tool via form (POST to /edit)
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-F "name=updated-tool" \
http://localhost:4444/admin/tools/$TOOL_ID/edit
# Same pattern for resources
curl -s -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
-F "name=updated-resource" \
http://localhost:4444/admin/resources/$RESOURCE_ID/editExpected Results:
- /admin/tools/{id} - GET returns JSON
- /admin/tools/{id}/edit - POST accepts form data
- /admin/resources/{id}/edit - POST accepts form data
TC-API-010: OpenAPI Schema Validation
Objective: Verify requests validated against schema
Steps:
# Step 1: Valid request
curl -s -w "\nStatus: %{http_code}\n" \
-X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Valid Tool"}' \
http://localhost:4444/tools
# Step 2: Invalid request (wrong type)
curl -s -w "\nStatus: %{http_code}\n" \
-X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":123}' \
http://localhost:4444/toolsExpected Results:
- Valid request: 201 Created
- Wrong type: 422 with validation error
TC-API-011: Extra Fields Handling
Objective: Verify extra fields are ignored
Steps:
# Request with extra fields
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"Tool","extra_field":"value","unknown":"data"}' \
http://localhost:4444/tools | jq '{name, extra_field, unknown}'Expected Results:
- Extra fields ignored (not stored)
- No error (Pydantic default behavior)
- No data pollution
TC-API-012: Team ID Fallback Behavior
Objective: Verify team_id falls back to schema value
Steps:
# Create tool with team_id in schema
curl -s -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"team-tool","team_id":"my-team","visibility":"team"}' \
http://localhost:4444/tools | jq '{name, team_id}'
# Note: Services fall back to schema team_id when endpoint doesn't provide one
# See: tool_service.py:1043-1044, resource_service.py:458Expected Results:
- team_id from schema is used if endpoint doesn't override
- Services fall back to schema value, NOT take precedence
Test Matrix
| Test Case | Mass Assign | BOLA | HPP | Validation | Endpoints |
|---|---|---|---|---|---|
| TC-API-001 | X | ||||
| TC-API-002 | X | ||||
| TC-API-003 | X | ||||
| TC-API-004 | X | ||||
| TC-API-005 | X | ||||
| TC-API-006 | X | ||||
| TC-API-007 | X | ||||
| TC-API-008 | X | ||||
| TC-API-009 | X | ||||
| TC-API-010 | X | ||||
| TC-API-011 | X | ||||
| TC-API-012 | X |
Success Criteria
- All 12 test cases pass
- created_by field set server-side, not by caller
- is_admin controlled by permissions (only admins can set)
- BOLA prevented (read, write, delete across teams)
- HTTP parameter pollution: LAST value used
- JSON APIs at /tools, /resources
- Form APIs at /admin/tools, /admin/resources
- Form update routes at /admin/tools/{id}/edit, /admin/resources/{id}/edit
- User endpoint uses email: /auth/email/admin/users/{email}
- team_id falls back to schema value when endpoint doesn't provide one
Related Files
mcpgateway/schemas.py- Pydantic schemas (team_id line 323, 1527; is_admin line 5204)mcpgateway/services/tool_service.py:1043-1044- team_id fallback logicmcpgateway/services/resource_service.py:458- team_id fallback logicmcpgateway/routers/email_auth.py:577,610,638- is_admin usage, user endpointmcpgateway/admin.py:9327,9337,9552,11181- Form data, /edit routesmcpgateway/main.py:3292- JSON API endpointmcpgateway/utils/create_jwt_token.py:341- --teams flagmcpgateway/middleware/rbac.py- Team-based access control
Review Corrections Applied
Reviewed: 2025-01-26 (Claude Opus 4.5 + Codex)
| Finding | Original | Corrected |
|---|---|---|
| team_id in schemas | Not in create schemas | IS in ToolCreate (line 323), ResourceCreate (line 1527) |
| is_admin in schemas | Not in create schemas | IS in EmailRegistrationRequest (line 5204) |
| HPP behavior | Uses first value | Uses LAST value (Starlette) |
| User endpoint | /api/users/{id} | /auth/email/admin/users/{user_email} |
| RBAC_ENABLED | In setup | Does NOT exist - use AUTH_REQUIRED |
| CLI flag | --team-id | --teams |
| /admin/tools | Accepts JSON | Expects FORM data |
| Port | 8000 | 4444 |
| Form update routes | /admin/tools/{id} | /admin/tools/{id}/edit (with /edit suffix) |
| team_id precedence | Token takes precedence | Services FALL BACK to schema team_id |
Related Issues
- None identified