Back

Software and Data Integrity Failures - Examples

Each pair below shows a vulnerable implementation and the secure version that verifies integrity. The examples cover the areas that dominate real findings: CI/CD pipelines, auto-update mechanisms in Node and Python, Java deserialization, and CDN/Subresource Integrity.

1. CI/CD Pipeline (GitHub Actions YAML)

Vulnerable

name: release
on: [push]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions: write-all            # every job can write everything
    steps:
      - uses: actions/checkout@main   # mutable ref: today's "main" runs in CI
      - uses: some-org/publish@v1     # mutable tag can be repointed to malware
      - run: npm install              # resolves floating ranges, runs any script
      - run: npm publish              # unsigned artifact, broad token in scope
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}   # long-lived, wide-scope secret

Problems: write-all permissions, mutable action refs, unpinned dependencies with install scripts, a long-lived publish token exposed to every step, and no artifact signing. Any compromised step can exfiltrate the token or inject code before publish.

Secure

name: release
on:
  push:
    tags: ['v*']                      # release only from reviewed, tagged commits

permissions:
  contents: read                      # least privilege by default

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: step-security/harden-runner@017...   # pinned SHA; block egress
        with:
          egress-policy: block
          allowed-endpoints: registry.npmjs.org:443 github.com:443
      - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608  # v4 pinned
      - run: npm ci                    # frozen, hash-verified install
      - run: npm run build && npm test

  publish:
    needs: build
    runs-on: ubuntu-latest
    environment: production            # requires approval / protected env
    permissions:
      id-token: write                  # short-lived OIDC for provenance
      contents: read
    steps:
      - uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608
      - run: npm ci
      # npm provenance ties the published package to THIS verified workflow:
      - run: npm publish --provenance --access public
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}   # scoped, isolated to this job

Fixes: read-only default permissions, pinned action SHAs, frozen dependency install, a separate protected publish job, short-lived OIDC provenance, and egress control so a poisoned step cannot phone home.

2. Auto-Update Mechanism (Node.js)

Vulnerable

const https = require('https');
const { execFile } = require('child_process');
const fs = require('fs');

// Downloads a binary and runs it. No signature, no version check.
function autoUpdate(manifestUrl) {
  https.get(manifestUrl, res => {
    let body = '';
    res.on('data', c => body += c);
    res.on('end', () => {
      const { url } = JSON.parse(body);         // trusts manifest blindly
      const file = fs.createWriteStream('/opt/app/update.bin');
      https.get(url, r => r.pipe(file).on('finish', () => {
        execFile('/opt/app/update.bin');        // executes whatever arrived
      }));
    });
  });
}

Problem: whatever the manifest points to is executed. An on-path attacker or a compromised mirror achieves remote code execution, often at the updater’s (elevated) privilege.

Secure

const https = require('https');
const crypto = require('crypto');
const fs = require('fs');

// Public key SHIPPED WITH THE APP (pinned); never fetched at runtime.
const PINNED_PUBKEY = fs.readFileSync(__dirname + '/release-ed25519.pub');
const CURRENT_VERSION = require('./version.json').version;

function verify(buf, sigB64) {
  return crypto.verify(null, buf, PINNED_PUBKEY, Buffer.from(sigB64, 'base64'));
}

async function autoUpdate(manifest) {
  // manifest = { version, url, sha256, artifactSig, manifestSig }
  const manifestBytes = Buffer.from(JSON.stringify({
    version: manifest.version, url: manifest.url, sha256: manifest.sha256,
  }));
  if (!verify(manifestBytes, manifest.manifestSig))
    throw new Error('manifest signature invalid');
  if (semverLte(manifest.version, CURRENT_VERSION))
    throw new Error('refusing rollback / replay');

  const artifact = await download(manifest.url);           // Buffer
  const digest = crypto.createHash('sha256').update(artifact).digest('hex');
  if (digest !== manifest.sha256)
    throw new Error('artifact hash mismatch');
  if (!verify(artifact, manifest.artifactSig))
    throw new Error('artifact signature invalid');

  fs.writeFileSync('/opt/app/update.bin', artifact);        // trusted only now
  install('/opt/app/update.bin');
}

Fixes: the manifest and artifact are both signature-verified against a pinned key, the hash is checked, and rollbacks are rejected. An unverifiable update is never written or executed.

3. Auto-Update Mechanism (Python)

Vulnerable

import requests, subprocess

def update():
    info = requests.get("http://updates.example.com/latest.json").json()
    blob = requests.get(info["url"]).content     # plain HTTP, no verification
    with open("/opt/app/plugin.py", "wb") as f:
        f.write(blob)
    subprocess.run(["python", "/opt/app/plugin.py"])   # runs untrusted code

Problem: plaintext channel and zero verification. Anyone on the path substitutes the payload and gains code execution.

Secure

import requests, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

# Pinned verification key compiled/shipped with the app:
PINNED_PUBKEY = Ed25519PublicKey.from_public_bytes(_SHIPPED_PUBKEY_BYTES)
CURRENT_VERSION = (5, 1, 0)

def _verify(data: bytes, sig: bytes):
    PINNED_PUBKEY.verify(sig, data)   # raises InvalidSignature on tampering

def update():
    # HTTPS is necessary but NOT sufficient; signatures are the real control.
    manifest = requests.get("https://updates.example.com/latest.json").json()
    manifest_bytes = f"{manifest['version']}|{manifest['url']}|{manifest['sha256']}".encode()
    try:
        _verify(manifest_bytes, bytes.fromhex(manifest["manifest_sig"]))
    except InvalidSignature:
        raise SystemExit("manifest signature invalid - aborting")

    if tuple(manifest["version"]) <= CURRENT_VERSION:
        raise SystemExit("refusing rollback / replay")

    blob = requests.get(manifest["url"]).content
    if hashlib.sha256(blob).hexdigest() != manifest["sha256"]:
        raise SystemExit("artifact hash mismatch")
    try:
        _verify(blob, bytes.fromhex(manifest["artifact_sig"]))
    except InvalidSignature:
        raise SystemExit("artifact signature invalid - aborting")

    # Only reached if EVERYTHING verified:
    with open("/opt/app/plugin.py", "wb") as f:
        f.write(blob)

Fixes: HTTPS transport plus pinned-key signature verification of both manifest and artifact, a hash check, and rollback protection. Note that HTTPS alone would not have stopped a compromised mirror.

4. Deserialization (Java)

Vulnerable

// Reconstructs arbitrary objects from an attacker-controlled stream.
public Session load(byte[] data) throws Exception {
    ObjectInputStream ois =
        new ObjectInputStream(new ByteArrayInputStream(data));
    return (Session) ois.readObject();   // gadget chains -> RCE
}

Problem: native Java deserialization instantiates any serializable class on the classpath and runs its lifecycle methods, enabling well-known gadget-chain exploits.

Secure

// Option A (best): don't use native serialization. Use JSON + a fixed type.
public Session load(byte[] data) throws Exception {
    ObjectMapper mapper = new ObjectMapper();
    mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    return mapper.readValue(data, Session.class);   // data-only, fixed target type
}

// Option B (if native serialization is unavoidable): strict allow-list filter.
public Session loadFiltered(byte[] data) throws Exception {
    ObjectInputStream ois =
        new ObjectInputStream(new ByteArrayInputStream(data));
    ois.setObjectInputFilter(info -> {              // JEP 290
        Class<?> c = info.serialClass();
        if (c == null) return ObjectInputFilter.Status.UNDECIDED;
        return c == Session.class
            ? ObjectInputFilter.Status.ALLOWED
            : ObjectInputFilter.Status.REJECTED;    // everything else denied
    });
    return (Session) ois.readObject();
}

Fixes: prefer a data-only format bound to a known type; if native deserialization must remain, restrict it to an explicit class allow-list so gadget classes are rejected before instantiation.

5. Deserialization (Python pickle)

Vulnerable

import pickle, base64

@app.route("/restore")
def restore():
    raw = base64.b64decode(request.cookies["state"])
    return render(pickle.loads(raw))     # __reduce__ executes -> RCE

Secure

import json
from flask import request, abort
from pydantic import BaseModel, ValidationError

class State(BaseModel):
    view: str
    page: int

@app.route("/restore")
def restore():
    try:
        data = json.loads(request.cookies["state"])   # data only
        state = State(**data)                          # schema validated
    except (ValueError, ValidationError):
        abort(400)
    return render(state)

Fixes: replace pickle with data-only JSON and validate into a strict schema. The parser can never execute code, and malformed input is rejected.

6. CDN Script Integrity (SRI)

Vulnerable

<!-- Executes whatever the CDN returns, even if compromised -->
<script src="https://cdn.example.com/pay/2.4.0/checkout.js"></script>

Secure

<!-- Runs only if the fetched bytes match the pinned hash -->
<script src="https://cdn.example.com/pay/2.4.0/checkout.js"
        integrity="sha384-q8Wj5r2Fh0m3s...pinned-hash..."
        crossorigin="anonymous"></script>

<!-- Optional: enforce that scripts MUST carry SRI via CSP -->
<!-- Content-Security-Policy: require-sri-for script; -->

Fixes: the browser refuses to run a tampered file because its hash no longer matches the pinned integrity value—neutralising a compromised CDN or hijacked URL.

7. Trusted Client-Side State

Vulnerable

// Node/Express: reads price and role straight from a cookie
app.post('/checkout', (req, res) => {
  const total = Number(req.cookies.cart_total);   // user-editable
  const role  = req.cookies.role;                 // user-editable
  charge(total);                                  // "cart_total=0.01"
  if (role === 'admin') showAdminReceipt();       // "role=admin"
});

Secure

const crypto = require('crypto');
const KEY = process.env.STATE_HMAC_KEY;           // server-only secret

function seal(obj) {
  const body = Buffer.from(JSON.stringify(obj));
  const tag = crypto.createHmac('sha256', KEY).update(body).digest('hex');
  return body.toString('base64') + '.' + tag;
}
function unseal(token) {
  const [b64, tag] = token.split('.');
  const body = Buffer.from(b64, 'base64');
  const expected = crypto.createHmac('sha256', KEY).update(body).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(tag), Buffer.from(expected)))
    throw new Error('state tampering detected');
  return JSON.parse(body);
}

app.post('/checkout', (req, res) => {
  // Authoritative values come from the server; the cookie is only an opaque ref.
  const cart = loadCartFromDb(req.session.userId);   // price from DB, not cookie
  const role = req.session.role;                     // role from server session
  charge(cart.total);
});

Fixes: keep authoritative state (price, role) server-side; where client state is genuinely needed, seal it with an HMAC and verify it with a constant-time comparison so any edit is detected and rejected.

Summary

AreaVulnerable patternSecure pattern
CI/CDwrite-all, mutable tags, unsigned publishLeast privilege, pinned SHAs, provenance
Auto-updateDownload and executeVerify pinned-key signature + hash + version
DeserializationNative deserializer on user inputData-only format + schema / allow-list filter
CDN assetsBare <script src>SRI hash (+ CSP require-sri-for)
Client stateTrust cookie valuesServer-side state or HMAC-sealed + verified

Next Steps

---

Part of the OWASP Top 10 Educational Repository