Initial commit
This commit is contained in:
382
skills/auth-security/reference/jwt-security.md
Normal file
382
skills/auth-security/reference/jwt-security.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# JWT Security Best Practices
|
||||
|
||||
This reference provides comprehensive guidance for reviewing JWT (JSON Web Token) implementation and usage in pull requests.
|
||||
|
||||
## Critical Security Principles
|
||||
|
||||
### 1. Never Forward JWTs to Unintended Services
|
||||
|
||||
**The Problem:**
|
||||
A JWT contains an `aud` (audience) claim that specifies which service(s) the token is intended for. Forwarding a JWT to a service not listed in the audience claim creates serious security vulnerabilities.
|
||||
|
||||
**Security Impact:**
|
||||
- **Confused Deputy Attack**: The receiving service cannot verify that the forwarding service is authorized to act on behalf of the user
|
||||
- **Privilege Escalation**: If the receiving service accepts tokens not intended for it, attackers can misuse tokens from one service at another
|
||||
- **ALBEAST-Class Vulnerability**: Tokens intended for one tenant/service can be used at another
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ CRITICAL SECURITY ISSUE
|
||||
def call_external_api(user_token):
|
||||
# Forwarding user's JWT directly to third-party service
|
||||
response = requests.get(
|
||||
"https://third-party-api.com/resource",
|
||||
headers={"Authorization": f"Bearer {user_token}"}
|
||||
)
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL - Must be blocked before merge
|
||||
|
||||
### 2. Validate Audience Claims
|
||||
|
||||
Every service that accepts JWTs **MUST** validate the `aud` claim matches its own identifier.
|
||||
|
||||
**RFC Requirements:**
|
||||
- Per RFC 7519: "Each principal intended to process the JWT MUST identify itself with a value in the audience claim"
|
||||
- Per RFC 9068: "The resource server MUST validate that the aud claim contains a resource indicator value corresponding to an identifier the resource server expects for itself"
|
||||
- **The JWT MUST be rejected if the audience does not match**
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ MISSING VALIDATION
|
||||
def verify_token(token):
|
||||
decoded = jwt.decode(token, public_key, algorithms=['RS256'])
|
||||
# Missing audience validation!
|
||||
return decoded
|
||||
|
||||
# ✅ CORRECT VALIDATION
|
||||
def verify_token(token):
|
||||
decoded = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=['RS256'],
|
||||
audience='https://api.myservice.com' # Validates aud claim
|
||||
)
|
||||
return decoded
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL if missing, HIGH if incomplete
|
||||
|
||||
### 3. Validate All Critical Claims
|
||||
|
||||
Beyond audience, validate:
|
||||
|
||||
**Required Validations:**
|
||||
- `iss` (issuer): Verify the token came from a trusted authorization server
|
||||
- `aud` (audience): Verify the token is intended for this service
|
||||
- `exp` (expiration): Reject expired tokens
|
||||
- `nbf` (not before): Reject tokens used before their valid time
|
||||
- `alg` (algorithm): Prevent algorithm confusion attacks
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ VULNERABLE TO ALGORITHM CONFUSION
|
||||
def verify_token(token):
|
||||
# Accepts ANY algorithm from the token header
|
||||
decoded = jwt.decode(token, secret, algorithms=None)
|
||||
|
||||
# ✅ CORRECT - EXPLICIT ALGORITHM
|
||||
def verify_token(token):
|
||||
# Only accepts expected algorithm
|
||||
decoded = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=['RS256'], # Explicit, not from token
|
||||
issuer='https://auth.example.com',
|
||||
audience='https://api.example.com'
|
||||
)
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL for algorithm validation, HIGH for other claims
|
||||
|
||||
### 4. Use Token Exchange for Service-to-Service Communication
|
||||
|
||||
When a service needs to call another service on behalf of a user, **use OAuth Token Exchange (RFC 8693)** rather than forwarding the original token.
|
||||
|
||||
**The Correct Pattern:**
|
||||
```python
|
||||
# ✅ SECURE TOKEN EXCHANGE PATTERN
|
||||
def call_downstream_service(user_token):
|
||||
# Exchange user token for service-specific token
|
||||
exchange_response = requests.post(
|
||||
'https://auth.example.com/token',
|
||||
data={
|
||||
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
|
||||
'subject_token': user_token,
|
||||
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
|
||||
'resource': 'https://downstream-service.example.com',
|
||||
'scope': 'read:data' # Downscoped to minimum needed
|
||||
}
|
||||
)
|
||||
|
||||
downstream_token = exchange_response.json()['access_token']
|
||||
|
||||
# Use the service-specific token
|
||||
response = requests.get(
|
||||
'https://downstream-service.example.com/api/resource',
|
||||
headers={'Authorization': f'Bearer {downstream_token}'}
|
||||
)
|
||||
```
|
||||
|
||||
**Benefits of Token Exchange:**
|
||||
1. **Correct Audience**: New token has proper `aud` claim for downstream service
|
||||
2. **Least Privilege**: Token is downscoped to minimum permissions needed
|
||||
3. **Audit Trail**: Clear chain of delegation (user → service A → service B)
|
||||
4. **Prevents Confused Deputy**: Downstream service can validate the token was properly issued
|
||||
|
||||
**What to Look For in PRs:**
|
||||
- Services forwarding user JWTs to other services without exchange
|
||||
- Missing token exchange implementation where multi-service calls exist
|
||||
- Tokens with overly broad scopes being passed between services
|
||||
|
||||
**Severity:** CRITICAL in security-sensitive contexts, HIGH otherwise
|
||||
|
||||
### 5. Apply Principle of Least Privilege
|
||||
|
||||
**Scope Downscoping:**
|
||||
When exchanging tokens, request only the minimum scopes needed for the specific operation.
|
||||
|
||||
```python
|
||||
# ❌ OVER-PRIVILEGED
|
||||
exchange_data = {
|
||||
'scope': 'read write admin delete' # Too many permissions!
|
||||
}
|
||||
|
||||
# ✅ LEAST PRIVILEGE
|
||||
exchange_data = {
|
||||
'scope': 'read:invoices' # Only what's needed
|
||||
}
|
||||
```
|
||||
|
||||
**Severity:** MEDIUM to HIGH depending on the over-privileging
|
||||
|
||||
### 6. Secure Token Storage and Transmission
|
||||
|
||||
**Transport Security:**
|
||||
- JWTs MUST be transmitted over HTTPS only
|
||||
- Never log full JWTs (mask them if needed for debugging)
|
||||
- Never include JWTs in URLs (query parameters, path segments)
|
||||
|
||||
**Storage Security:**
|
||||
- Don't store JWTs in localStorage if they contain sensitive data (XSS risk)
|
||||
- Consider httpOnly cookies for web applications
|
||||
- Use secure, encrypted storage on mobile platforms
|
||||
- Implement token refresh to minimize long-lived token exposure
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```javascript
|
||||
// ❌ SECURITY ISSUES
|
||||
localStorage.setItem('token', jwt); // XSS vulnerability
|
||||
console.log('Token:', jwt); // Logging sensitive data
|
||||
const url = `/api/resource?token=${jwt}`; // Token in URL
|
||||
|
||||
// ✅ BETTER PRACTICES
|
||||
// Use httpOnly cookie or secure storage
|
||||
document.cookie = `token=${jwt}; Secure; HttpOnly; SameSite=Strict`;
|
||||
console.log('Token:', jwt.substring(0, 10) + '...'); // Masked logging
|
||||
```
|
||||
|
||||
**Severity:** HIGH for URL exposure, MEDIUM for storage issues
|
||||
|
||||
## Algorithm Confusion Attacks
|
||||
|
||||
### The Vulnerability
|
||||
|
||||
JWT headers include an `alg` parameter that specifies the signing algorithm. If the verification code doesn't enforce the expected algorithm, attackers can:
|
||||
|
||||
1. Change RS256 (RSA) to HS256 (HMAC)
|
||||
2. Use the RSA public key as the HMAC secret
|
||||
3. Create validly-signed tokens
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ VULNERABLE
|
||||
def verify_token(token):
|
||||
# Algorithm taken from token header - DANGEROUS!
|
||||
header = jwt.get_unverified_header(token)
|
||||
alg = header['alg']
|
||||
decoded = jwt.decode(token, key, algorithms=[alg])
|
||||
|
||||
# ✅ SECURE
|
||||
def verify_token(token):
|
||||
# Algorithm explicitly specified and validated
|
||||
decoded = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=['RS256'] # Only RS256 accepted
|
||||
)
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL
|
||||
|
||||
### None Algorithm Attack
|
||||
|
||||
Some JWT libraries accept `"alg": "none"` which bypasses signature verification entirely.
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ VULNERABLE
|
||||
jwt.decode(token, verify=False) # Dangerous!
|
||||
jwt.decode(token, algorithms=['RS256', 'none']) # Allows none!
|
||||
|
||||
# ✅ SECURE
|
||||
jwt.decode(token, public_key, algorithms=['RS256'])
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL
|
||||
|
||||
## Common JWT Anti-Patterns
|
||||
|
||||
### 1. Treating ID Tokens as Access Tokens
|
||||
|
||||
**Problem:** ID tokens (from OpenID Connect) are meant for the client that requested authentication, NOT for API authorization.
|
||||
|
||||
```python
|
||||
# ❌ WRONG
|
||||
# Forwarding ID token to API
|
||||
api_call(headers={'Authorization': f'Bearer {id_token}'})
|
||||
|
||||
# ✅ CORRECT
|
||||
# Use access token for API calls
|
||||
api_call(headers={'Authorization': f'Bearer {access_token}'})
|
||||
```
|
||||
|
||||
**Severity:** HIGH
|
||||
|
||||
### 2. Missing Signature Validation
|
||||
|
||||
**Problem:** Accepting unsigned JWTs or skipping validation.
|
||||
|
||||
```python
|
||||
# ❌ VULNERABLE
|
||||
payload = jwt.decode(token, options={"verify_signature": False})
|
||||
|
||||
# ✅ SECURE
|
||||
payload = jwt.decode(token, public_key, algorithms=['RS256'])
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL
|
||||
|
||||
### 3. Trusting Token Content Without Validation
|
||||
|
||||
**Problem:** Extracting claims before validating signature and claims.
|
||||
|
||||
```python
|
||||
# ❌ DANGEROUS
|
||||
def get_user_id(token):
|
||||
# Decodes without validation!
|
||||
payload = jwt.decode(token, options={"verify_signature": False})
|
||||
return payload['user_id']
|
||||
|
||||
# ✅ SAFE
|
||||
def get_user_id(token):
|
||||
# Validates first, then extracts
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=['RS256'],
|
||||
audience='https://api.example.com',
|
||||
issuer='https://auth.example.com'
|
||||
)
|
||||
return payload['user_id']
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL
|
||||
|
||||
### 4. Using Weak Secrets for HS256
|
||||
|
||||
**Problem:** Using predictable or weak secrets for HMAC signing.
|
||||
|
||||
```python
|
||||
# ❌ WEAK SECRET
|
||||
secret = "secret123"
|
||||
jwt.encode(payload, secret, algorithm='HS256')
|
||||
|
||||
# ✅ STRONG SECRET
|
||||
# Use cryptographically random, high-entropy secret
|
||||
# At least 256 bits (32 bytes) for HS256
|
||||
import secrets
|
||||
secret = secrets.token_bytes(32)
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL
|
||||
|
||||
### 5. Overly Broad Audiences
|
||||
|
||||
**Problem:** Using wildcard or overly generic audience values.
|
||||
|
||||
```python
|
||||
# ❌ TOO BROAD
|
||||
token_data = {
|
||||
'aud': '*', # Accepts anywhere!
|
||||
'aud': 'https://example.com' # Too generic
|
||||
}
|
||||
|
||||
# ✅ SPECIFIC
|
||||
token_data = {
|
||||
'aud': 'https://api.example.com/v1/orders' # Specific service
|
||||
}
|
||||
```
|
||||
|
||||
**Severity:** MEDIUM to HIGH
|
||||
|
||||
## Review Checklist for JWT Code
|
||||
|
||||
When reviewing PRs involving JWT authentication/authorization:
|
||||
|
||||
### Critical Checks
|
||||
- [ ] **Audience validation**: Every service validates `aud` claim matches its identifier
|
||||
- [ ] **Algorithm enforcement**: Explicit algorithm list, no `none`, no user-controlled algorithm
|
||||
- [ ] **Signature validation**: All tokens are cryptographically validated
|
||||
- [ ] **Issuer validation**: `iss` claim validated against trusted issuers
|
||||
- [ ] **No token forwarding**: Tokens are not forwarded to services not in their `aud` claim
|
||||
|
||||
### High Priority Checks
|
||||
- [ ] **Token exchange**: Service-to-service calls use token exchange, not forwarding
|
||||
- [ ] **Expiration validation**: `exp` claim checked and enforced
|
||||
- [ ] **Scope downscoping**: Exchanged tokens request minimum necessary scopes
|
||||
- [ ] **ID token misuse**: ID tokens not used for API authorization
|
||||
- [ ] **Transport security**: Tokens only transmitted over HTTPS
|
||||
|
||||
### Medium Priority Checks
|
||||
- [ ] **Least privilege**: Scopes are specific and minimal
|
||||
- [ ] **Secure storage**: Tokens stored securely (not localStorage for sensitive data)
|
||||
- [ ] **Token refresh**: Short-lived tokens with refresh mechanism
|
||||
- [ ] **Logging safety**: Tokens not logged in full
|
||||
- [ ] **Error messages**: Don't leak token information in errors
|
||||
|
||||
### Code Pattern Recognition
|
||||
|
||||
**Token Forwarding Pattern (Usually Wrong):**
|
||||
```python
|
||||
# Server receives user_token from client
|
||||
# Server forwards user_token to another service
|
||||
downstream_service.call(authorization=user_token) # ❌ Check this!
|
||||
```
|
||||
|
||||
**Token Exchange Pattern (Usually Correct):**
|
||||
```python
|
||||
# Server receives user_token from client
|
||||
# Server exchanges for service-specific token
|
||||
new_token = auth_server.exchange_token(user_token, target_service)
|
||||
# Server uses new token
|
||||
downstream_service.call(authorization=new_token) # ✅ Good!
|
||||
```
|
||||
|
||||
## Key RFCs and Standards
|
||||
|
||||
- **RFC 7519**: JSON Web Token (JWT) - Core standard
|
||||
- **RFC 8693**: OAuth 2.0 Token Exchange - Service-to-service delegation
|
||||
- **RFC 8707**: Resource Indicators for OAuth 2.0 - Explicit audience specification
|
||||
- **RFC 9068**: JWT Profile for OAuth 2.0 Access Tokens - Access token best practices
|
||||
- **RFC 9700**: OAuth 2.0 Security Best Current Practice (January 2025) - Latest security guidance
|
||||
|
||||
## When to Escalate
|
||||
|
||||
Escalate to security team or mark as CRITICAL if you find:
|
||||
- Tokens forwarded to services not in their audience
|
||||
- Missing signature validation
|
||||
- Algorithm confusion vulnerabilities (accepting `none` or user-controlled algorithm)
|
||||
- Tokens containing sensitive data logged or exposed in URLs
|
||||
- No audience or issuer validation
|
||||
- Token exchange not used in multi-service architectures
|
||||
571
skills/auth-security/reference/mcp-authorization.md
Normal file
571
skills/auth-security/reference/mcp-authorization.md
Normal file
@@ -0,0 +1,571 @@
|
||||
# MCP (Model Context Protocol) Authorization Security
|
||||
|
||||
This reference provides comprehensive guidance for reviewing MCP server implementations with a focus on OAuth 2.1 authorization and security best practices.
|
||||
|
||||
## Overview
|
||||
|
||||
The Model Context Protocol (MCP) specification finalized OAuth 2.1-based authorization in June 2025. MCP servers represent high-value targets because they:
|
||||
- Store authentication tokens for multiple services
|
||||
- Execute actions across connected services
|
||||
- Can be invoked by AI agents with varying levels of user oversight
|
||||
- Potentially have access to sensitive data and operations
|
||||
|
||||
**Key Security Principle:** MCP servers must implement strict authorization controls to prevent unauthorized access and token misuse.
|
||||
|
||||
## MCP Authorization Requirements
|
||||
|
||||
### 1. OAuth 2.1 Compliance (MANDATORY)
|
||||
|
||||
The MCP specification requires OAuth 2.1 as the authorization foundation.
|
||||
|
||||
**Core Requirements:**
|
||||
- MUST use OAuth 2.1 (not OAuth 2.0)
|
||||
- MUST implement PKCE (Proof Key for Code Exchange) for all clients
|
||||
- MUST implement Resource Indicators (RFC 8707)
|
||||
- MUST validate tokens with correct audience claims
|
||||
- MUST NOT use sessions for authentication
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ NON-COMPLIANT - OAuth 2.0 without PKCE
|
||||
@app.route('/authorize')
|
||||
def authorize():
|
||||
redirect_uri = request.args.get('redirect_uri')
|
||||
# Missing code_challenge and code_challenge_method
|
||||
auth_code = generate_auth_code()
|
||||
return redirect(f'{redirect_uri}?code={auth_code}')
|
||||
|
||||
# ✅ COMPLIANT - OAuth 2.1 with PKCE
|
||||
@app.route('/authorize')
|
||||
def authorize():
|
||||
code_challenge = request.args.get('code_challenge')
|
||||
code_challenge_method = request.args.get('code_challenge_method')
|
||||
|
||||
if not code_challenge or code_challenge_method != 'S256':
|
||||
return {'error': 'invalid_request', 'error_description': 'PKCE required'}, 400
|
||||
|
||||
auth_code = generate_auth_code(code_challenge)
|
||||
return redirect(f'{redirect_uri}?code={auth_code}')
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL if PKCE missing, HIGH if OAuth 2.0 instead of 2.1
|
||||
|
||||
### 2. Resource Indicators (RFC 8707) - MANDATORY
|
||||
|
||||
MCP clients MUST implement Resource Indicators to explicitly specify the target MCP server.
|
||||
|
||||
**Purpose:**
|
||||
- Ensures tokens are audience-restricted to specific MCP servers
|
||||
- Prevents token misuse across different MCP servers
|
||||
- Enables proper multi-tenant security
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ MISSING RESOURCE INDICATOR
|
||||
token_request = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': auth_code,
|
||||
'redirect_uri': redirect_uri,
|
||||
# Missing 'resource' parameter!
|
||||
}
|
||||
|
||||
# ✅ CORRECT - WITH RESOURCE INDICATOR
|
||||
token_request = {
|
||||
'grant_type': 'authorization_code',
|
||||
'code': auth_code,
|
||||
'redirect_uri': redirect_uri,
|
||||
'resource': 'https://mcp-server.example.com', # Explicit target
|
||||
'code_verifier': code_verifier # PKCE
|
||||
}
|
||||
```
|
||||
|
||||
**Audience Validation:**
|
||||
```python
|
||||
# ✅ MCP SERVER MUST VALIDATE AUDIENCE
|
||||
def validate_token(token):
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=['RS256'],
|
||||
audience='https://mcp-server.example.com', # Must match resource
|
||||
issuer='https://auth.example.com'
|
||||
)
|
||||
return payload
|
||||
except jwt.InvalidAudienceError:
|
||||
raise HTTPException(403, 'Token not intended for this MCP server')
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL if missing
|
||||
|
||||
### 3. Strict Token Acceptance Policy
|
||||
|
||||
**MCP servers MUST NOT accept tokens that were not explicitly issued for them.**
|
||||
|
||||
This is the core defense against confused deputy attacks.
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ VULNERABLE - ACCEPTS ANY TOKEN
|
||||
def authorize_request(request):
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
# No audience validation!
|
||||
payload = jwt.decode(token, public_key, algorithms=['RS256'])
|
||||
return payload['user_id']
|
||||
|
||||
# ✅ SECURE - VALIDATES AUDIENCE
|
||||
def authorize_request(request):
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
public_key,
|
||||
algorithms=['RS256'],
|
||||
audience=MCP_SERVER_IDENTIFIER, # This server's identifier
|
||||
issuer=TRUSTED_ISSUER
|
||||
)
|
||||
except jwt.InvalidAudienceError:
|
||||
raise Forbidden('Token not issued for this MCP server')
|
||||
|
||||
return payload
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL
|
||||
|
||||
### 4. Scope Validation and Insufficient Scope Response
|
||||
|
||||
MCP servers MUST validate that tokens have the required scopes for operations.
|
||||
|
||||
**Required Behavior:**
|
||||
- When a token has insufficient scope, respond with HTTP 403 Forbidden
|
||||
- Include `WWW-Authenticate` header with `error="insufficient_scope"`
|
||||
- Optionally include `scope` parameter indicating required scopes
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ MISSING SCOPE VALIDATION
|
||||
@app.route('/tools/invoke', methods=['POST'])
|
||||
def invoke_tool(token_payload):
|
||||
# No scope check!
|
||||
result = execute_tool(request.json['tool_name'])
|
||||
return result
|
||||
|
||||
# ✅ CORRECT SCOPE VALIDATION
|
||||
@app.route('/tools/invoke', methods=['POST'])
|
||||
def invoke_tool(token_payload):
|
||||
required_scope = 'tools.invoke'
|
||||
|
||||
if required_scope not in token_payload.get('scope', '').split():
|
||||
response = Response(
|
||||
'Insufficient scope',
|
||||
status=403,
|
||||
headers={
|
||||
'WWW-Authenticate': f'Bearer error="insufficient_scope", scope="{required_scope}"'
|
||||
}
|
||||
)
|
||||
return response
|
||||
|
||||
result = execute_tool(request.json['tool_name'])
|
||||
return result
|
||||
```
|
||||
|
||||
**Severity:** HIGH
|
||||
|
||||
### 5. No Session-Based Authentication
|
||||
|
||||
**MCP servers MUST NOT use sessions for authentication.**
|
||||
|
||||
This is a specific requirement of the MCP specification.
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ VIOLATES MCP SPEC - USING SESSIONS
|
||||
@app.route('/tools/list')
|
||||
def list_tools():
|
||||
if 'user_id' not in session:
|
||||
return {'error': 'unauthorized'}, 401
|
||||
# Using Flask session - NOT ALLOWED
|
||||
|
||||
# ✅ COMPLIANT - TOKEN-BASED
|
||||
@app.route('/tools/list')
|
||||
def list_tools():
|
||||
token = request.headers.get('Authorization', '').replace('Bearer ', '')
|
||||
payload = validate_mcp_token(token) # OAuth 2.1 token validation
|
||||
return get_tools_for_user(payload['sub'])
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL - violates MCP specification
|
||||
|
||||
### 6. Secure Session ID Generation (if used for OAuth flow)
|
||||
|
||||
If the MCP server or authorization server uses session IDs during the OAuth flow (not for authentication):
|
||||
|
||||
**Requirements:**
|
||||
- MUST use secure, non-deterministic session IDs
|
||||
- MUST use cryptographically secure random number generators
|
||||
- MUST have sufficient entropy to prevent guessing
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ WEAK SESSION ID
|
||||
import random
|
||||
session_id = str(random.randint(1000, 9999)) # Predictable!
|
||||
|
||||
# ✅ SECURE SESSION ID
|
||||
import secrets
|
||||
session_id = secrets.token_urlsafe(32) # Cryptographically secure
|
||||
```
|
||||
|
||||
**Severity:** HIGH
|
||||
|
||||
## Token Forwarding to MCP Servers
|
||||
|
||||
### The Critical Security Issue
|
||||
|
||||
A common anti-pattern is forwarding user authentication tokens from an inference server (or other service) to an MCP server.
|
||||
|
||||
**Example of Vulnerable Flow:**
|
||||
```
|
||||
User → Inference Server (with user JWT)
|
||||
↓
|
||||
Inference Server → MCP Server (forwarding user JWT) ❌ INSECURE
|
||||
```
|
||||
|
||||
**Why This Is Dangerous:**
|
||||
|
||||
1. **Audience Mismatch**: User JWT has `aud` claim for inference server, not MCP server
|
||||
2. **Confused Deputy**: MCP server cannot verify inference server is authorized to delegate
|
||||
3. **No Scope Restriction**: MCP server receives user's full permissions, not downscoped
|
||||
4. **Violates MCP Spec**: MCP servers must only accept tokens issued for them
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ❌ CRITICAL VULNERABILITY
|
||||
class InferenceServer:
|
||||
def call_mcp_tool(self, user_token, tool_name, params):
|
||||
# Forwarding user token directly to MCP server!
|
||||
response = requests.post(
|
||||
f'{MCP_SERVER_URL}/tools/invoke',
|
||||
headers={'Authorization': f'Bearer {user_token}'}, # WRONG!
|
||||
json={'tool': tool_name, 'params': params}
|
||||
)
|
||||
return response.json()
|
||||
```
|
||||
|
||||
**Severity:** CRITICAL - Must be blocked before merge
|
||||
|
||||
### The Correct Pattern: Token Exchange
|
||||
|
||||
**Secure Flow:**
|
||||
```
|
||||
User → Inference Server (with user JWT)
|
||||
↓ (validate user JWT)
|
||||
Inference Server → Auth Server (token exchange request)
|
||||
↓ (receives MCP-scoped token)
|
||||
Inference Server → MCP Server (with MCP-specific token) ✓ SECURE
|
||||
```
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ✅ CORRECT - TOKEN EXCHANGE
|
||||
class InferenceServer:
|
||||
def call_mcp_tool(self, user_token, tool_name, params):
|
||||
# Step 1: Validate user token
|
||||
user_claims = self.validate_user_token(user_token)
|
||||
|
||||
# Step 2: Exchange for MCP-specific token
|
||||
mcp_token = self.exchange_token_for_mcp(
|
||||
user_token=user_token,
|
||||
mcp_server='https://mcp-server.example.com',
|
||||
required_scopes=['tools.invoke']
|
||||
)
|
||||
|
||||
# Step 3: Use MCP-specific token
|
||||
response = requests.post(
|
||||
f'{MCP_SERVER_URL}/tools/invoke',
|
||||
headers={'Authorization': f'Bearer {mcp_token}'}, # Correct audience
|
||||
json={'tool': tool_name, 'params': params}
|
||||
)
|
||||
return response.json()
|
||||
|
||||
def exchange_token_for_mcp(self, user_token, mcp_server, required_scopes):
|
||||
"""Exchange user token for MCP-server-specific token"""
|
||||
exchange_response = requests.post(
|
||||
f'{AUTH_SERVER_URL}/token',
|
||||
data={
|
||||
'grant_type': 'urn:ietf:params:oauth:grant-type:token-exchange',
|
||||
'subject_token': user_token,
|
||||
'subject_token_type': 'urn:ietf:params:oauth:token-type:access_token',
|
||||
'resource': mcp_server, # RFC 8707 Resource Indicator
|
||||
'scope': ' '.join(required_scopes) # Downscoped
|
||||
}
|
||||
)
|
||||
|
||||
if exchange_response.status_code != 200:
|
||||
raise AuthorizationError('Token exchange failed')
|
||||
|
||||
return exchange_response.json()['access_token']
|
||||
```
|
||||
|
||||
**Key Security Benefits:**
|
||||
1. ✅ MCP token has correct `aud` claim for the MCP server
|
||||
2. ✅ Token is downscoped to minimum needed permissions
|
||||
3. ✅ Clear audit trail preserved (user → inference → MCP)
|
||||
4. ✅ Complies with MCP OAuth 2.1 specification
|
||||
5. ✅ Prevents confused deputy attacks
|
||||
6. ✅ MCP server can properly validate the token
|
||||
|
||||
**Severity:** Implementation of token exchange is CRITICAL when missing
|
||||
|
||||
## MCP Server Architecture Patterns
|
||||
|
||||
### Pattern 1: Embedded Authorization Server
|
||||
|
||||
The MCP server includes its own auth system.
|
||||
|
||||
**Characteristics:**
|
||||
- Handles login, user sessions, consent UI
|
||||
- Issues and verifies tokens
|
||||
- Acts as both authorization server and resource server
|
||||
|
||||
**Security Considerations:**
|
||||
- Must implement full OAuth 2.1 spec
|
||||
- Requires secure user credential management
|
||||
- Needs proper consent UI for user authorization
|
||||
- Session management during OAuth flow (not for MCP operations)
|
||||
|
||||
### Pattern 2: External Authorization Server (Recommended)
|
||||
|
||||
The MCP server delegates OAuth to a trusted external provider.
|
||||
|
||||
**Characteristics:**
|
||||
- Authorization server handles authentication, user management, consent
|
||||
- MCP server only validates tokens and enforces scopes
|
||||
- Clear separation of concerns
|
||||
|
||||
**Security Benefits:**
|
||||
- Leverages battle-tested auth infrastructure
|
||||
- Reduces attack surface of MCP server
|
||||
- Easier to audit and maintain
|
||||
- Better compliance with OAuth 2.1 spec
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ✅ EXTERNAL AUTH SERVER PATTERN
|
||||
class MCPServer:
|
||||
def __init__(self, auth_server_url, mcp_server_id):
|
||||
self.auth_server_url = auth_server_url
|
||||
self.mcp_server_id = mcp_server_id
|
||||
# Fetch public keys from auth server
|
||||
self.public_keys = self.fetch_jwks()
|
||||
|
||||
def validate_token(self, token):
|
||||
"""Validate token issued by external auth server"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
self.public_keys,
|
||||
algorithms=['RS256'],
|
||||
audience=self.mcp_server_id,
|
||||
issuer=self.auth_server_url
|
||||
)
|
||||
return payload
|
||||
except jwt.InvalidTokenError as e:
|
||||
raise Unauthorized(str(e))
|
||||
|
||||
def fetch_jwks(self):
|
||||
"""Fetch JSON Web Key Set from auth server"""
|
||||
response = requests.get(f'{self.auth_server_url}/.well-known/jwks.json')
|
||||
return response.json()
|
||||
```
|
||||
|
||||
## Transport Security
|
||||
|
||||
### Mutual TLS (mTLS)
|
||||
|
||||
For production MCP deployments, especially in enterprise environments:
|
||||
|
||||
**Requirement:** Transport security SHOULD use mutual TLS (mTLS) for bidirectional verification.
|
||||
|
||||
**What to Look For in PRs:**
|
||||
```python
|
||||
# ✅ mTLS CONFIGURATION
|
||||
import ssl
|
||||
|
||||
context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
context.load_cert_chain(certfile='server.crt', keyfile='server.key')
|
||||
context.load_verify_locations(cafile='client_ca.crt')
|
||||
|
||||
# Use context in server configuration
|
||||
server = HTTPServer(('localhost', 8443), MCPRequestHandler)
|
||||
server.socket = context.wrap_socket(server.socket, server_side=True)
|
||||
```
|
||||
|
||||
**Severity:** MEDIUM for internal deployments, HIGH for external-facing servers
|
||||
|
||||
### HTTPS Mandatory
|
||||
|
||||
**All MCP communication MUST occur over HTTPS (or equivalent secure transport).**
|
||||
|
||||
**What to Look For in PRs:**
|
||||
- HTTP URLs in production configurations (red flag)
|
||||
- Missing TLS/SSL certificate validation
|
||||
- Disabled certificate verification (`verify=False`)
|
||||
|
||||
**Severity:** CRITICAL if HTTP used in production
|
||||
|
||||
## Security Risks Specific to MCP
|
||||
|
||||
### 1. High-Value Target
|
||||
|
||||
MCP servers store tokens for multiple services. If compromised:
|
||||
- Attacker gains access to all connected service tokens
|
||||
- Can execute actions across all integrated services
|
||||
- Potential for lateral movement across services
|
||||
|
||||
**Mitigation:**
|
||||
- Encrypt tokens at rest
|
||||
- Use short-lived tokens with refresh
|
||||
- Implement rate limiting and anomaly detection
|
||||
- Audit all token usage
|
||||
|
||||
### 2. Prompt Injection Attacks
|
||||
|
||||
AI agents can be manipulated through prompt injection to:
|
||||
- Execute unintended MCP tool invocations
|
||||
- Access unauthorized resources
|
||||
- Exfiltrate data through tool responses
|
||||
|
||||
**What to Look For in PRs:**
|
||||
- Input validation on tool parameters
|
||||
- Scope restrictions preventing dangerous operations
|
||||
- Audit logging of all tool invocations
|
||||
- User confirmation for sensitive operations
|
||||
|
||||
**Severity:** HIGH to CRITICAL depending on MCP server capabilities
|
||||
|
||||
### 3. Cross-Prompt Injection
|
||||
|
||||
Malicious content embedded in documents or UI can override agent instructions.
|
||||
|
||||
**Mitigation:**
|
||||
- Sanitize external content before processing
|
||||
- Implement content security policies
|
||||
- Separate user instructions from external content
|
||||
- Require explicit user confirmation for sensitive actions
|
||||
|
||||
**Severity:** HIGH
|
||||
|
||||
### 4. Remote Code Execution Risk
|
||||
|
||||
If MCP tools can execute code or system commands, prompt injection could lead to RCE.
|
||||
|
||||
**What to Look For in PRs:**
|
||||
- Code execution capabilities without sandboxing
|
||||
- System command execution from tool parameters
|
||||
- File system access without path validation
|
||||
- Network access without destination whitelisting
|
||||
|
||||
**Severity:** CRITICAL
|
||||
|
||||
## Review Checklist for MCP Code
|
||||
|
||||
When reviewing PRs involving MCP server implementation:
|
||||
|
||||
### Critical Checks
|
||||
- [ ] **OAuth 2.1 compliance**: Using OAuth 2.1 (not 2.0)
|
||||
- [ ] **PKCE mandatory**: All authorization flows use PKCE with S256
|
||||
- [ ] **Resource Indicators**: Token requests include explicit `resource` parameter
|
||||
- [ ] **Audience validation**: Server validates `aud` claim matches its identifier
|
||||
- [ ] **No token forwarding**: Server doesn't accept tokens issued for other services
|
||||
- [ ] **Token exchange**: Uses token exchange for upstream service calls
|
||||
- [ ] **No session auth**: Doesn't use sessions for MCP operation authentication
|
||||
|
||||
### High Priority Checks
|
||||
- [ ] **Scope validation**: Validates required scopes and returns proper insufficient_scope errors
|
||||
- [ ] **Issuer validation**: Validates `iss` claim against trusted issuers
|
||||
- [ ] **Algorithm enforcement**: Explicit algorithm list (no `none`, no user-controlled)
|
||||
- [ ] **Signature validation**: All tokens cryptographically validated
|
||||
- [ ] **HTTPS/TLS**: All communication over secure transport
|
||||
- [ ] **Input validation**: Tool parameters validated and sanitized
|
||||
|
||||
### Medium Priority Checks
|
||||
- [ ] **Token storage**: Tokens encrypted at rest
|
||||
- [ ] **Audit logging**: Tool invocations logged with user context
|
||||
- [ ] **Rate limiting**: Prevents abuse of MCP endpoints
|
||||
- [ ] **Least privilege**: Scopes are minimal and specific
|
||||
- [ ] **mTLS consideration**: mTLS for production deployments
|
||||
- [ ] **Secure session IDs**: If used in OAuth flow, cryptographically random
|
||||
|
||||
### MCP-Specific Security Patterns
|
||||
|
||||
**Inference Server + MCP Architecture:**
|
||||
```
|
||||
✅ SECURE PATTERN:
|
||||
1. User authenticates to inference server
|
||||
2. Inference server validates user token
|
||||
3. Inference server exchanges token for MCP-specific token
|
||||
4. Inference server invokes MCP with proper token
|
||||
5. MCP server validates audience matches its identifier
|
||||
|
||||
❌ INSECURE PATTERN:
|
||||
1. User authenticates to inference server
|
||||
2. Inference server forwards user token to MCP
|
||||
3. MCP server accepts token not issued for it
|
||||
```
|
||||
|
||||
## Platform-Specific Considerations
|
||||
|
||||
### Windows 11 MCP Proxy
|
||||
|
||||
Windows 11 provides centralized MCP proxy for security:
|
||||
|
||||
**Features:**
|
||||
- Proxy-mediated communication for all MCP interactions
|
||||
- Centralized policy enforcement
|
||||
- Consistent authentication and authorization
|
||||
- Enhanced audit and monitoring
|
||||
|
||||
**What to Look For in PRs:**
|
||||
- Windows platform should integrate with Windows 11 MCP proxy
|
||||
- Proxy configuration and policy settings
|
||||
- Proper certificate validation for proxy communication
|
||||
|
||||
**Severity:** MEDIUM for Windows deployments
|
||||
|
||||
## Key Standards and Specifications
|
||||
|
||||
- **MCP Authorization Spec** (June 2025): Official OAuth 2.1 authorization for MCP
|
||||
- **OAuth 2.1**: Required authorization framework
|
||||
- **RFC 7636**: PKCE - Mandatory for all MCP clients
|
||||
- **RFC 8707**: Resource Indicators - Mandatory for MCP token requests
|
||||
- **RFC 8693**: Token Exchange - Recommended for service-to-service delegation
|
||||
- **RFC 9068**: JWT Profile for OAuth 2.0 Access Tokens
|
||||
- **RFC 9700**: OAuth 2.0 Security Best Current Practice (January 2025)
|
||||
|
||||
## When to Escalate
|
||||
|
||||
Escalate to security team or mark as CRITICAL if you find:
|
||||
|
||||
1. **Token forwarding**: Inference/API servers forwarding user tokens to MCP servers
|
||||
2. **Missing audience validation**: MCP server accepts tokens without validating `aud` claim
|
||||
3. **No PKCE**: Authorization flow missing PKCE implementation
|
||||
4. **No Resource Indicators**: Token requests missing `resource` parameter
|
||||
5. **Session-based auth**: Using sessions for MCP operation authentication
|
||||
6. **Prompt injection risks**: MCP tools with code execution and no sandboxing
|
||||
7. **Missing OAuth 2.1**: Still using OAuth 2.0 instead of 2.1
|
||||
8. **HTTP communication**: Production MCP server using HTTP instead of HTTPS
|
||||
|
||||
## Summary
|
||||
|
||||
MCP authorization security requires:
|
||||
1. **OAuth 2.1** with PKCE (mandatory)
|
||||
2. **Resource Indicators** (RFC 8707) for explicit audience targeting
|
||||
3. **Strict token validation** (audience, issuer, signature, scopes)
|
||||
4. **Token exchange** for service-to-service delegation (not forwarding)
|
||||
5. **No session-based authentication** for MCP operations
|
||||
6. **Secure transport** (HTTPS/TLS, preferably mTLS)
|
||||
7. **Input validation** and prompt injection protection
|
||||
|
||||
The most common vulnerability is token forwarding from inference servers to MCP servers. This must be replaced with proper token exchange flows.
|
||||
Reference in New Issue
Block a user