Back

Software and Data Integrity Failures - Prevention

Defense Strategy: Verify Before You Trust

Every defense in this category is a variation on one rule: never act on an artifact or piece of data until you have verified, against a trusted reference, that it is genuine and unmodified. The trusted reference is a hash you already know, or a public key you already trust—delivered through a channel independent of the artifact itself.

The layers below map onto the four faces of integrity failure. They are additive: dependency pinning does not protect your update channel, and signed updates do not protect your deserialization endpoints. Apply all of them.

LayerProtects againstPrimary control
Dependency integrityTyposquat, confusion, poisoned versionsLockfiles with pinned hashes, verified registries, SCA
Subresource integrityCompromised CDN / third-party scriptSRI hashes on browser assets
Secure CI/CDBuild-pipeline injectionLeast privilege, review, isolation, pinned actions, provenance
Signed updatesMalicious / rolled-back updatesSignature verification against a pinned key + version checks
Safe data handlingDeserialization RCE, tampered stateSafe formats, schema validation, HMAC on client state

Layer 1: Dependency & Package Integrity

Pin to verified hashes with a committed lockfile

A lockfile records the exact version and cryptographic hash of every resolved dependency. Committing it and installing in “frozen” mode means the build fails if any package’s content no longer matches the recorded hash.

# Node.js: install strictly from the lockfile; fail on any mismatch
npm ci                     # refuses to modify package-lock.json; verifies integrity

# package-lock.json records a Subresource-Integrity-style hash per package:
# "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz",
# "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA=="
# Python (pip): require hashes for every package
pip install --require-hashes -r requirements.txt

# requirements.txt with pinned hashes:
# requests==2.31.0 \
#   --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1

Prevent dependency confusion

# npm: scope internal packages and pin the registry for that scope
# .npmrc
@acme:registry=https://registry.internal.acme.com/
# Public registry can never satisfy @acme/* -> confusion attack blocked

Run Software Composition Analysis (SCA)

# Fail the build on known-vulnerable or policy-violating dependencies
npm audit --audit-level=high
pip-audit -r requirements.txt
osv-scanner --lockfile=package-lock.json

SCA overlaps with A06:2021 Vulnerable & Outdated Components (known CVEs). Here we use it additionally to catch integrity signals—unexpected new packages, yanked versions, and provenance gaps.

Layer 2: Subresource Integrity for Browser Assets

Subresource Integrity (SRI) lets the browser verify that a script or stylesheet fetched from a CDN matches a hash you embedded in your HTML. If the CDN is compromised, the hash no longer matches and the browser refuses to execute the file.

<!-- Vulnerable: browser trusts whatever the CDN serves -->
<script src="https://cdn.example.com/lib/3.7.1/lib.min.js"></script>

<!-- Secure: browser executes ONLY if the content hash matches -->
<script src="https://cdn.example.com/lib/3.7.1/lib.min.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>
# Generate the SRI hash for a file you are pinning:
cat lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A
# Prefix the output with "sha384-" for the integrity attribute.

Layer 3: Secure CI/CD Pipelines

The build system is trusted to produce your release, so it is exactly where a SolarWinds-class attacker wants to be. Treat the pipeline as production infrastructure.

Least privilege and secret hygiene

Pin actions/plugins to immutable digests

# Vulnerable: mutable tag can be silently repointed to malicious code
- uses: actions/checkout@v4

# Secure: pin to an immutable commit SHA
- uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608  # v4.1.0

Harden the pipeline and enforce review

# Example GitHub Actions job hardened for integrity
permissions:
  contents: read            # default to read-only; grant more only per-job
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: step-security/harden-runner@... # egress control on the runner
        with:
          egress-policy: block
          allowed-endpoints: registry.npmjs.org:443 github.com:443
      - uses: actions/checkout@<sha>
      - run: npm ci                          # frozen, hash-verified install
      - run: npm run build
      # Signing happens in a separate, higher-trust job with restricted access

Generate and verify build provenance

# Sign an artifact and produce provenance you can later verify (Sigstore cosign):
cosign sign-blob --yes app-5.2.0.bin --output-signature app-5.2.0.bin.sig

# Consumers verify it came from your trusted identity BEFORE trusting it:
cosign verify-blob \
  --signature app-5.2.0.bin.sig \
  --certificate-identity "https://github.com/acme/app/.github/workflows/release.yml@refs/tags/v5.2.0" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  app-5.2.0.bin

Provenance frameworks such as SLSA formalize “how was this artifact built, and can I verify it?” The deeper supply-chain governance (SBOMs, org-wide policy) is covered in the 2025 Software Supply Chain Failures lesson; here the goal is that your own pipeline produces tamper-evident, verifiable releases.

Layer 4: Signed, Verified Updates

An auto-updater is remote code execution by design—it downloads code and runs it. The only thing that makes that safe is verifying a signature over the update against a public key you shipped with the application.

# Secure update flow (pseudocode):
1. Fetch update manifest over HTTPS
2. Verify the manifest signature with the PINNED public key
3. Reject if manifest.version <= installed.version   # block rollback
4. Download the artifact
5. Recompute the artifact hash; compare to the signed hash in the manifest
6. Verify the artifact signature with the PINNED public key
7. ONLY THEN install/execute
# Python: verify a detached signature before applying an update
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature

PINNED_PUBKEY = Ed25519PublicKey.from_public_bytes(SHIPPED_PUBLIC_KEY_BYTES)

def apply_update(artifact: bytes, signature: bytes, new_version, cur_version):
    if new_version <= cur_version:
        raise SecurityError("refusing rollback / replay")
    try:
        PINNED_PUBKEY.verify(signature, artifact)   # raises on tampering
    except InvalidSignature:
        raise SecurityError("update signature invalid - aborting")
    install(artifact)                               # trusted only after verify

Layer 5: Safe Deserialization & Trusted Data

Do not deserialize untrusted data with native deserializers

The most reliable defense is architectural: never feed attacker-controllable bytes to pickle.loads, Java ObjectInputStream, PHP unserialize, or .NET BinaryFormatter. Use a data-only format and reconstruct your own objects explicitly.

# Vulnerable: native object deserialization of user input
import pickle
state = pickle.loads(base64.b64decode(request.cookies['state']))  # RCE risk

# Secure: parse data-only JSON, then validate into a known schema
import json
from pydantic import BaseModel, ValidationError

class CartState(BaseModel):
    item_ids: list[int]
    coupon: str | None = None

raw = json.loads(request.cookies['state'])      # data only, no code execution
try:
    state = CartState(**raw)                     # strict schema validation
except ValidationError:
    abort(400)

If you must round-trip state through the client, sign it

# Bind client-held state with an HMAC and verify on every request
import hmac, hashlib, json

def seal(state: dict, key: bytes) -> str:
    body = json.dumps(state, separators=(',', ':')).encode()
    tag = hmac.new(key, body, hashlib.sha256).hexdigest()
    return base64.b64encode(body).decode() + '.' + tag

def unseal(token: str, key: bytes) -> dict:
    b64, tag = token.split('.', 1)
    body = base64.b64decode(b64)
    expected = hmac.new(key, body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(tag, expected):   # constant-time compare
        raise SecurityError("state tampering detected")
    return json.loads(body)

Verify tokens correctly

# JWT: pin algorithms and always verify
import jwt   # PyJWT
claims = jwt.decode(token, PUBLIC_KEY, algorithms=["RS256"])  # explicit allow-list
# Never: jwt.decode(token, options={"verify_signature": False})

Harden Java / .NET deserialization if it cannot be removed

Consolidated Checklist

Next Steps

---

Part of the OWASP Top 10 Educational Repository