Skip to content

[TESTING][SECURITY]: API security manual test plan (mass assignment, BOLA, parameter pollution, OpenAPI validation) #2412

@crivetimihai

Description

@crivetimihai

[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:

  1. Production Readiness: Security must be validated before release
  2. Compliance: Required by security standards and audits
  3. Defense in Depth: Validates multiple protection layers
  4. Attack Mitigation: Prevents common exploitation techniques
  5. 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 caller

Note: 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 returned

Story 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 possible

Story 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_ID

Expected 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_ID

Expected 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 .name

Expected 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/edit

Expected 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/tools

Expected 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:458

Expected 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 logic
  • mcpgateway/services/resource_service.py:458 - team_id fallback logic
  • mcpgateway/routers/email_auth.py:577,610,638 - is_admin usage, user endpoint
  • mcpgateway/admin.py:9327,9337,9552,11181 - Form data, /edit routes
  • mcpgateway/main.py:3292 - JSON API endpoint
  • mcpgateway/utils/create_jwt_token.py:341 - --teams flag
  • mcpgateway/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

Metadata

Metadata

Assignees

Labels

MUSTP1: Non-negotiable, critical requirements without which the product is non-functional or unsafechoreLinting, formatting, dependency hygiene, or project maintenance choresmanual-testingManual testing / test planning issuesreadyValidated, ready-to-work-on itemssecurityImproves securitytestingTesting (unit, e2e, manual, automated, etc)

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions