diff --git a/docs/Chapter_17_Plugin_and_API_Exploitation.md b/docs/Chapter_17_Plugin_and_API_Exploitation.md index da486b1..caf9891 100644 --- a/docs/Chapter_17_Plugin_and_API_Exploitation.md +++ b/docs/Chapter_17_Plugin_and_API_Exploitation.md @@ -1002,6 +1002,300 @@ def test_oauth_flow(): ## JWT token security +**Understanding JWT for LLM Plugins:** + +JSON Web Tokens (JWT) are self-contained tokens that carry authentication and authorization information. Unlike session IDs that require database lookups, JWTs are stateless—all necessary data is encoded in the token itself. This makes them ideal for distributed LLM plugin systems where centralized session storage would be a bottleneck. + +**JWT Structure:** + +A JWT consists of three parts separated by dots: + +``` +header.payload.signature +``` + +Example: + +``` +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInBlcm1pc3Npb25zIjpbInJlYWQiXX0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +``` + +**Decoded:** + +1. **Header** (Base64-encoded JSON): + + ```json + { "alg": "HS256", "typ": "JWT" } + ``` + + - `alg`: Signing algorithm (HMAC SHA256) + - `typ`: Token type + +2. **Payload** (Base64-encoded JSON): + + ```json + { + "user_id": 123, + "permissions": ["read"], + "iat": 1640000000, + "exp": 1640086400, + "jti": "unique-token-id" + } + ``` + + - `user_id`: User identifier + - `permissions`: Authorization claims + - `iat`: Issued at (Unix timestamp) + - `exp`: Expiration (Unix timestamp) + - `jti`: JWT ID (for revocation) + +3. **Signature** (Cryptographic hash): + ``` + HMACSHA256( + base64UrlEncode(header) + "." + base64UrlEncode(payload), + secret_key + ) + ``` + +**Why JWTs are Useful:** + +✅ **Stateless**: No database lookup required for validation +✅ **Scalable**: Can be validated by any server with the secret key +✅ **Self-Contained**: All user info embedded in token +✅ **Cross-Domain**: Works across different services/plugins +✅ **Standard**: RFC 7519, widely supported + +**How This Implementation Works:** + +**1. Token Creation:** + +```python +def create_token(self, user_id, permissions, expiration_hours=24): + payload = { + 'user_id': user_id, + 'permissions': permissions, + 'iat': time.time(), # When token was issued + 'exp': time.time() + (expiration_hours * 3600), # When it expires + 'jti': secrets.token_urlsafe(16) # Unique token ID + } + token = jwt.encode(payload, self.secret_key, algorithm=self.algorithm) + return token +``` + +**Key claims explained:** + +- **iat (Issued At)**: Prevents token replay attacks from the past +- **exp (Expiration)**: Limits token lifetime (typically 1-24 hours) +- **jti (JWT ID)**: Unique identifier for token revocation (stored in blacklist) + +**2. Token Validation:** + +```python +def validate_token(self, token): + try: + payload = jwt.decode( + token, + self.secret_key, + algorithms=[self.algorithm] # CRITICAL: Specify allowed algorithms + ) +``` + +**Why `algorithms=[self.algorithm]` is critical:** + +Without this, attacker can change `alg` in header to `none` or `HS256` when expecting `RS256`, bypassing signature verification entirely. This is called the **algorithm confusion attack**. + +**Algorithm Confusion Attack Example:** + +```python +# Vulnerable code (no algorithm specification) +payload = jwt.decode(token, secret_key) # ❌ DANGEROUS + +# Attacker creates token with alg=none: +malicious_token = base64_encode('{"alg":"none"}') + '.' + base64_encode('{"user_id":1,"permissions":["admin"]}') + '.' + +# Server accepts it because no algorithm was enforced! +# Result: Attacker has admin access without valid signature +``` + +**Secure version:** + +```python +payload = jwt.decode(token, secret_key, algorithms=['HS256']) # ✅ SAFE +# If token uses different algorithm → InvalidTokenError +``` + +**3. Expiration Check:** + +```python +if payload['exp'] < time.time(): + raise TokenExpiredError() +``` + +Even if signature is valid, reject expired tokens. This limits the damage if a token is stolen—it only works until expiration. + +**4. Revocation Check:** + +```python +if self.is_token_revoked(payload['jti']): + raise TokenRevokedError() +``` + +JWTs are stateless, but you can maintain a blacklist of revoked `jti` values (in Redis or database). This allows manual token revocation when: + +- User logs out +- Account is compromised +- Permissions change + +**Common JWT Vulnerabilities:** + +**1. Algorithm Confusion (alg=none)** + +- **Attack**: Change `alg` to `none`, remove signature +- **Defense**: Always specify `algorithms` parameter in decode + +**2. Weak Secret Keys** + +```python +# ❌ Bad: Easily brute-forced +secret_key = "secret123" + +# ✅ Good: Strong random key +secret_key = secrets.token_urlsafe(64) +``` + +**3. No Expiration** + +```python +# ❌ Bad: Token never expires +payload = {'user_id': 123} # Missing 'exp' + +# ✅ Good: Short expiration +payload = {'user_id': 123, 'exp': time.time() + 3600} # 1 hour +``` + +**4. Storing Sensitive Data** + +```python +# ❌ Bad: JWT payloads are Base64-encoded, NOT encrypted +payload = {'user_id': 123, 'password': 'secret123'} # Visible to anyone! + +# ✅ Good: Only non-sensitive data +payload = {'user_id': 123, 'permissions': ['read']} +``` + +**5. Not Validating Claims** + +```python +# ❌ Bad: Accept any valid JWT +payload = jwt.decode(token, secret_key, algorithms=['HS256']) + +# ✅ Good: Validate issuer, audience +payload = jwt.decode( + token, + secret_key, + algorithms=['HS256'], + issuer='myapp.com', # Only accept tokens from our app + audience='api.myapp.com' # Only for our API +) +``` + +**Security Best Practices:** + +1. **Use strong cryptographic secrets**: + + ```python + import secrets + SECRET_KEY = secrets.token_urlsafe(64) # 512 bits of entropy + ``` + +2. **Short expiration times**: + + ```python + 'exp': time.time() + 900 # 15 minutes for access tokens + ``` + + Use refresh tokens for longer sessions. + +3. **Rotate secrets regularly**: + + ```python + # Support multiple keys for rotation + KEYS = { + 'key1': 'old-secret', + 'key2': 'current-secret' + } + + # Try all keys when validating + for key_id, key in KEYS.items(): + try: + return jwt.decode(token, key, algorithms=['HS256']) + except jwt.InvalidTokenError: + continue + ``` + +4. **Include audience and issuer**: + + ```python + payload = { + 'iss': 'myapp.com', # Issuer + 'aud': 'api.myapp.com', # Audience + 'sub': 'user123', # Subject (user ID) + 'exp': time.time() + 3600 + } + ``` + +5. **Use RS256 for public/private key scenarios**: + + ```python + # When multiple services need to validate tokens + # but shouldn't be able to create them + + # Token creation (private key) + token = jwt.encode(payload, private_key, algorithm='RS256') + + # Token validation (public key) + payload = jwt.decode(token, public_key, algorithms=['RS256']) + ``` + +**HS256 vs RS256:** + +| Feature | HS256 (HMAC) | RS256 (RSA) | +| ----------- | ------------------------- | ---------------------------------- | +| Key Type | Shared secret | Public/Private keypair | +| Use Case | Single service | Multiple services | +| Signing | Same key signs & verifies | Private key signs, public verifies | +| Security | Secret must be protected | Private key must be protected | +| Performance | Faster | Slower (asymmetric crypto) | + +**When to use RS256:** + +- Multiple plugins need to validate tokens +- Don't want to share secret with all plugins +- Public key distribution is acceptable + +**Token Storage:** + +```python +# ✅ Good: HTTP-only cookie (not accessible via JavaScript) +response.set_cookie( + 'jwt_token', + token, + httponly=True, # Prevents XSS attacks + secure=True, # HTTPS only + samesite='Strict' # CSRF protection +) + +# ❌ Bad: localStorage (vulnerable to XSS) +localStorage.setItem('jwt_token', token) # JavaScript can access! +``` + +**Prerequisites:** + +- Understanding of cryptographic signatures +- Familiarity with Base64 encoding +- Knowledge of token-based authentication +- Awareness of common JWT vulnerabilities + ```python import jwt import time @@ -1012,6 +1306,7 @@ class JWTTokenManager: def __init__(self, secret_key, algorithm='HS256'): self.secret_key = secret_key self.algorithm = algorithm + self.revocation_list = set() # Initialize revocation list def create_token(self, user_id, permissions, expiration_hours=24): """Create JWT token""" @@ -1035,17 +1330,22 @@ class JWTTokenManager: algorithms=[self.algorithm] ) - # Check if token is revoked - if self.is_revoked(payload['jti']): + # Check expiration + if payload['exp'] < time.time(): + raise TokenExpiredError() + + # Verify not revoked + if self.is_token_revoked(payload['jti']): raise TokenRevokedError() return payload - - except jwt.ExpiredSignatureError: - raise TokenExpiredError() except jwt.InvalidTokenError: raise InvalidTokenError() + def is_token_revoked(self, jti): + """Check if a token is in the revocation list""" + return jti in self.revocation_list + def revoke_token(self, jti): """Revoke specific token""" self.revocation_list.add(jti)