

Disclaimer: This guide is for educational and defensive purposes only. The vulnerabilities discussed relate to improper handling of serialized objects or unsafe deserialization during the authentication handshake between identity providers (IdPs) and local services. The code provided demonstrates how to implement secure validation patterns to prevent such exploits.
Recent vulnerabilities in Belgium's eID authentication workflows highlighted a critical flaw: when the authentication token (containing citizen identity metadata) is parsed using unsafe deserialization methods, an attacker can inject malicious objects. If the backend processes these objects without strict schema validation, it leads to Remote Code Execution (RCE).
This guide provides a production-ready implementation for developers building identity-integrated applications, focusing on Strict Schema Validation and Safe Parsing to prevent these attacks.
Before implementing secure authentication handlers, ensure your environment meets these requirements:
pip (Python) or npm/yarn (JavaScript).pytest or jest for unit testing security boundaries.Install the necessary dependencies for secure parsing and validation.
# Install core security and validation libraries
pip install pydantic cryptography PyJWT python-dotenv
# Install core security and validation libraries
npm install jsonwebtoken pydantic-js dotenv zod
# For TypeScript users
npm install --save-dev @types/jsonwebtoken @types/node
To prevent RCE, we must never use pickle (Python) or eval() (JS) on identity data. Instead, we use Strict Schema Validation via Pydantic (Python) or Zod (TypeScript).
import jwt # PyJWT
from pydantic import BaseModel, ValidationError, Field
from typing import Dict, Any
import os
from dotenv import load_dotenv
load_dotenv()
# 1. Define a strict Schema for the eID Metadata
# This prevents "Mass Assignment" and "Unexpected Object Injection"
class CitizenIdentitySchema(BaseModel):
citizen_id: str = Field(..., pattern=r"^[A-Z0-9]{12}$") # Strict regex for eID format
full_name: str = Field(..., min_length=3, max_length=100)
email: str
is_verified: bool = False
class AuthenticationService:
def __init__(self, secret_key: str):
self.secret_key = secret_key
def validate_eid_token(self, token: str) -> CitizenIdentitySchema:
"""
Safely decodes and validates the eID token.
Prevents RCE by enforcing strict schema validation after decoding.
"""
try:
# 2. Securely decode the JWT
# We explicitly define allowed algorithms to prevent 'alg: none' attacks
payload = jwt.decode(
token,
self.secret_key,
algorithms=["HS256"]
)
# 3. Validate payload against the Schema
# This is the critical step to prevent RCE/Injection
validated_data = CitizenIdentitySchema(**payload)
return validated_data
except jwt.exceptions.InvalidTokenError as e:
print(f"Security Alert: Invalid Token Attempt: {e}")
raise ValueError("Authentication failed: Invalid token.")
except ValidationError as e:
print(f"Security Alert: Malicious Payload Detected: {e.json()}")
raise ValueError("Authentication failed: Malicious data structure.")
except Exception as e:
print(f"Unexpected System Error: {e}")
raise ValueError("Internal authentication error.")
# --- TEST SUITE ---
if __name__ == "__main__":
SECRET = os.getenv("JWT_SECRET", "super-secret-key-123")
auth_service = AuthenticationService(SECRET)
# Valid Token Scenario
valid_payload = {
"citizen_id": "BE1234567890",
"full_name": "Jean Dupont",
"email": "jean.dupont@example.be",
"is_verified": True
}
token = jwt.encode(valid_payload, SECRET, algorithm="HS256")
print("Testing valid token...")
print(f"Result: {auth_service.validate_eid_token(token)}")
# Attack Scenario: Attempted RCE via unexpected object injection
malicious_payload = {
"citizen_id": "BE1234567890",
"full_name": "Attacker",
"email": "hacker@evil.com",
"exploit_payload": {"__class__": "os.system", "cmd": "rm -rf /"} # Attempted injection
}
malicious_token = jwt.encode(malicious_payload, SECRET, algorithm="HS256")
print("\nTesting malicious injection...")
try:
auth_service.validate_eid_token(malicious_token)
except ValueError as e:
print(f"Caught expected error: {e}")
import jwt from 'jsonwebtoken';
import { z } from 'zod'; // Zod for schema validation
import dotenv from 'dotenv';
dotenv.config();
// 1. Define a strict Zod Schema
// This ensures that ONLY expected fields are accepted.
const CitizenSchema = z.object({
citizen_id: z.string().regex(/^[A-Z0-9]{12}$/),
full_name: z.string().min(3).max(100),
email: z.string().email(),
is_verified: z.boolean().default(false),
}).strict(); //.strict() prevents extra fields (Anti-RCE measure)
interface IdentityResponse {
citizen_id: string;
full_name: string;
email: string;
is_verified: boolean;
}
class AuthService {
private secret: string;
constructor(secret: string) {
this.secret = secret;
}
/**
* Validates the eID JWT and ensures no malicious properties exist.
*/
public validateEidToken(token: string): IdentityResponse {
try {
// 2. Decode with strict algorithm check
const decoded = jwt.verify(token, this.secret, { algorithms: ['HS256'] }) as Record<string, any>;
// 3. Validate using Zod
// If an attacker injects '__proto__' or 'constructor',.strict() will throw an error
const validatedData = CitizenSchema.parse(decoded);
return validatedData as IdentityResponse;
} catch (error) {
if (error instanceof z.ZodError) {
console.error("SECURITY ALERT: Malicious payload detected!", error.errors);
throw new Error("Invalid identity data format.");
}
console.error("Authentication failed:", error);
throw new Error("Authentication failed.");
}
}
}
// --- TEST SUITE ---
const authService = new AuthService(process.env.JWT_SECRET || 'uper-secret-key-123');
// Valid Case
const validToken = jwt.sign(
{ citizen_id: 'BE1234567890', full_name: 'Jean Dupont', email: 'jean@test.be', is_verified: true },
'uper-secret-key-123'
);
console.log("Valid Token Result:", authService.validateEidToken(validToken));
// Attack Case: Injection attempt
const maliciousToken = jwt.sign(
{
citizen_id: 'BE1234567890',
full_name: 'Attacker',
email: 'hacker@evil.com',
"__proto__": { "admin": true } // Prototype Pollution Attempt
},
'uper-secret-key-123'
);
console.log("\nTesting Malicious Token...");
try {
authService.validateEidToken(maliciousToken);
} catch (e: any) {
console.log("Caught expected error:", e.message);
}
Never hardcode secrets. Use environment variables to manage your authentication keys.
.env file:
# Production: Use a 256-bit or higher random string
JWT_SECRET=your_ultra_secure_random_long_string_here
NODE_ENV=production
LOG_LEVEL=warn
Instead of accepting a generic JSON object, always map the incoming identity to a strictly defined Data Transfer Object (DTO). This is the primary defense against RCE.
Always specify the allowed algorithms in your jwt.verify or jwt.decode calls. This prevents an attacker from switching the algorithm to none or using an asymmetric public key to sign a symmetric token.
If validation fails, do not provide detailed error messages to the client (to prevent information leakage), but log the full error internally for security auditing.
| Error | Cause | Solution |
|---|---|---|
ValidationError | The incoming payload contains unexpected fields or wrong types. | Check if the eID provider changed their payload format or if an attack is occurring. |
InvalidTokenError | Signature mismatch or expired token. | Verify JWT_SECRET and check the exp (expiration) claim. |
ZodError / Pydantic Error | Schema mismatch. | Ensure your CitizenSchema matches the official eID specification exactly. |
TypeError: Cannot read property... | Attempted Prototype Pollution. | Ensure you are using .strict() in Zod or strict typing in Python. |
pickle.loads() or eval() is never used on data coming from the eID system.algorithms=['HS256'] is explicitly declared.JWT_SECRET is loaded from a secure Vault/Environment, never committed to Git.Source: Dark Reading
Follow ICARAX for more AI insights and tutorials.
