GUIDE · OWASP Top 10:2025
A08:2025 Software or Data Integrity Failures
Code or data is trusted without verifying it hasn't been tampered with — by the client, the network, or the pipeline.
At a glance
- WHAT
- Code or data is trusted without verifying it hasn't been tampered with — by the client, the network, or the pipeline.
- HAPPENS
- insecure deserialization (eval / node-serialize / JSON with __proto__ -> prototype pollution in JS); unsigned auto-updates; CI/CD pipeline that builds from unverified sources or lets PRs mutate the pipeline; CDN scripts without SRI; trusting client-side state (price in hidden field, role in cookie, isAdmin in JWT body that isn't verified); JWT signature skipped (
jwt.decode); cache poisoning; unsigned webhooks. - SPOT
- eval / new Function / vm.runInContext on input;
JSON.parsethen deep merge / Object.assign into defaults; lodash.merge / _.set with user keys; <script src="https://cdn..."> with no integrity=; req.body.price used in charge(); jwt.decode(token) used as truth; update URL over http; webhook handler with no signature check. - FIX
- never eval; parse with a reviver / Object.create(null) / deny __proto__, constructor, prototype keys; Object.freeze (Object.prototype) in hardened runtimes; SRI integrity attr + crossorigin; HMAC-signed tokens/cookies verified with timingSafeEqual; verify JWT signature + alg allowlist; server-side source of truth for prices/roles; signed releases (Sigstore); pipeline: protected branches, required reviews, no secrets in fork builds, verified artifacts.
- SAY
- CWE-502 Deserialization of Untrusted Data, CWE-1321 Prototype Pollution, CWE-345 Insufficient Verification of Data Authenticity, CWE-353 Missing Support for Integrity Check, CWE-494 Download of Code Without Integrity Check, CWE-347 Improper Verification of Cryptographic Signature. Pitch: "Verify before you trust: sign what crosses a boundary and check the signature on the way in."
The idea#
Picture a permission slip for a field trip. The words say yes. You trust the slip because of the signature on the line, and you hold that signature up to the one the office keeps on file. A slip with a blank line, or a signature that belongs to different words, stays in the tray. The class goes on the trip when the signature matches.
That is a Software or Data Integrity Failure when the match never happens. Code or data crossed a boundary, and something trusted it with no check that it was still the same bytes. Verify before you trust. Skip __proto__, constructor, and prototype when you merge outside JSON, and copy out only the fields you named. Keep prices and roles on the server. The client sends a sku. Sign a cookie with HMAC and compare with timingSafeEqual before you read it. Give a third-party script an integrity hash. Install an update, and accept a webhook, only after the signature checks.
Failure looks like a slip accepted because the paper felt official. A merge copies a dangerous key and changes objects across the process. Text from the client is evaluated as a program. A hidden price is the amount you charge. A cookie that is only base64 is decoded, and the role inside is obeyed. A script tag points at a shared host and has no hash, so a swapped file runs for every visitor. An update arrives with nothing to verify. A webhook is parsed, and the signature is somebody else's problem.
Why it matters#
Integrity asks whether this is still what you agreed to. A private message can still be edited in transit. A public file can still announce that its bytes changed. This 2025 category is that second promise breaking. The damage is a price, a role, or a program that changed after you last looked, while the system treated the new bytes as the original.
Check at the boundary. The browser's copy stays untrusted until the signature matches the key you hold. A merge stays untrusted until every key is one you meant to copy. Code you run comes from a template or a signed artifact. JSON.parse hands you data. The signature check tells you who produced it. An algorithm allowlist, passed into verify, is the decision.
A small example#
const crypto = require('node:crypto'); function sign(body, key) { const mac = crypto.createHmac('sha256', key).update(body).digest('base64url'); return body + '.' + mac;} function readSigned(token, key) { const dot = token.lastIndexOf('.'); if (dot < 0) return null; const body = token.slice(0, dot); const mac = Buffer.from(token.slice(dot + 1)); const want = Buffer.from( crypto.createHmac('sha256', key).update(body).digest('base64url') ); if (mac.length !== want.length) return null; if (!crypto.timingSafeEqual(mac, want)) return null; return body;} const key = crypto.randomBytes(32);const token = sign('uid=1', key);const body = readSigned(token, key);body is uid=1 because the MAC matches this key. A token with no dot, or a MAC that fails the compare, comes back null. You read the fields after that check returns the body.
What the code below is doing#
Six sections. Each one withholds trust until a check passes.
Merging is safeMerge and safeParse. Both skip __proto__, constructor, and prototype. safeMerge copies own keys, recurses only into plain objects, and builds children with Object.create(null). The asserts show Object.prototype still clean and the ordinary field ok.deep kept. safeParse drops those keys in a JSON.parse reviver.
Orders are parseOrder. After safeParse, sku must be a string and qty an integer of at least 1. The return value has only those two fields. A quantity that arrived as a string throws.
Prices are priceOrder over a catalog map. The unit price comes from the map. A price property in the JSON never reaches the math, because parseOrder did not copy it.
Cookies are signCookie and readCookie. The body is base64url JSON, the tail is HMAC-SHA256, and the compare is timingSafeEqual. A rewritten body under the old MAC returns null. The comment gives a JWT the same treatment: verify, with an algorithm allowlist.
Script tags are sriTag. It hashes the file with sha384 and writes integrity plus crossorigin="anonymous". The browser recomputes the hash and refuses the script when the bytes differ.
The last block is a checklist: signed updates, protected branches, secrets kept off untrusted fork builds, hashed artifacts, deploys of what the pipeline built, and webhook signatures checked with timingSafeEqual on the raw body before any parse.
When you talk about it, say this: sign what crosses a boundary, and check the signature before you trust what is inside.
Everything below is the demonstration. Same idea, written as code you can run.
'use strict';const assert = require('node:assert/strict');const crypto = require('node:crypto'); Not run in the browser: uses node:crypto. Copy the node command and run it locally.
Prototype pollution via deserialization + merge#
// VULNERABLE:// function merge(target, src) { for (const k in src) {// target[k] = (typeof src[k] === 'object') ? merge(target[k] || {}, src[k]) : src[k]; } return target; }// merge(config, JSON.parse('{"__proto__":{"isAdmin":true}}')); // now ({}).isAdmin === true everywhere// FIXED: deny the three dangerous keys; only recurse into plain objects; use null-prototype targets.const FORBIDDEN_KEYS = new Set(['__proto__', 'constructor', 'prototype']);function isPlainObject(v) { if (v === null || typeof v !== 'object' || Array.isArray(v)) return false; const p = Object.getPrototypeOf(v); return p === Object.prototype || p === null;}function safeMerge(target, src) { if (!isPlainObject(src)) return target; for (const key of Object.keys(src)) { // own enumerable only if (FORBIDDEN_KEYS.has(key)) continue; const val = src[key]; if (isPlainObject(val)) { if (!isPlainObject(target[key]) || !Object.hasOwn(target, key)) target[key] = Object.create(null); safeMerge(target[key], val); } else { target[key] = val; } } return target;}const evil = JSON.parse('{"__proto__":{"polluted":"yes"},"constructor":{"prototype":{"polluted2":"yes"}},"ok":{"deep":1}}');const cfg = safeMerge(Object.create(null), evil);// JSON.parse reviver variant: drop the keys at parse timefunction safeParse(text) { return JSON.parse(text, (k, v) => (FORBIDDEN_KEYS.has(k) ? undefined : v));}const parsed = safeParse('{"a":1,"__proto__":{"x":1},"b":{"constructor":{"y":2}}}');// Mitigations also: Object.freeze(Object.prototype) at startup; --disable-proto=delete; Map instead of {} for user keys.Verified by 8 asserts
assert.equal({}.polluted, undefined); // Object.prototype untouchedassert.equal({}.polluted2, undefined);assert.equal(Object.prototype.polluted, undefined);assert.equal(cfg.ok.deep, 1);assert.equal(Object.hasOwn(cfg, '__proto__'), false);assert.equal(Object.hasOwn(cfg, 'constructor'), false);assert.deepEqual(parsed, { a: 1, b: {} });assert.equal({}.x, undefined);Insecure deserialization: eval / new Function#
// VULNERABLE:// const obj = eval('(' + req.body.data + ')'); // RCE// const fn = new Function('return ' + req.query.expr)(); // RCE// node-serialize .unserialize(cookie) // IIFE in payload => RCE (classic CVE)// FIXED: JSON.parse only (data, not code), then validate the shape against a schema.function parseOrder(text) { const o = safeParse(text); if (!isPlainObject(o)) throw new TypeError('order must be an object'); if (typeof o.sku !== 'string' || !Number.isInteger(o.qty) || o.qty < 1) throw new TypeError('bad order'); return { sku: o.sku, qty: o.qty }; // allowlist the fields you copy out}Verified by 3 asserts
assert.deepEqual(parseOrder('{"sku":"a1","qty":2,"isAdmin":true}'), { sku: 'a1', qty: 2 });assert.throws(() => parseOrder('{"sku":"a1","qty":"2"}'), /bad order/);assert.throws(() => parseOrder('"string"'), /must be an object/);Trusting client-side state (price in hidden field / role in body)#
// VULNERABLE:// charge(req.user, req.body.price * req.body.qty); // attacker posts price=0.01// if (req.body.role === 'admin') grantAdmin();// FIXED: server-side source of truth; the client only sends identifiers.const catalog = new Map([['a1', 1999]]);function priceOrder({ sku, qty }) { const unit = catalog.get(sku); if (unit === undefined) throw new Error('unknown sku'); return unit * qty;}Verified by 1 assert
assert.equal(priceOrder(parseOrder('{"sku":"a1","qty":2,"price":1}')), 3998); // client "price" ignoredHMAC-signed cookie / token verified with timingSafeEqual#
// VULNERABLE: cookie `prefs=eyJ1aWQiOjEsInJvbGUiOiJhZG1pbiJ9` (base64 JSON) read back and trusted.// FIXED:const SECRET = crypto.randomBytes(32);function signCookie(obj, key = SECRET) { const body = Buffer.from(JSON.stringify(obj)).toString('base64url'); return body + '.' + crypto.createHmac('sha256', key).update(body).digest('base64url');}function readCookie(cookie, key = SECRET) { const dot = cookie.lastIndexOf('.'); if (dot < 0) return null; const body = cookie.slice(0, dot); const mac = Buffer.from(cookie.slice(dot + 1)); const want = Buffer.from(crypto.createHmac('sha256', key).update(body).digest('base64url')); if (mac.length !== want.length || !crypto.timingSafeEqual(mac, want)) return null; return safeParse(Buffer.from(body, 'base64url').toString('utf8'));}const c = signCookie({ uid: 1, role: 'user' });const forged = Buffer.from(JSON.stringify({ uid: 1, role: 'admin' })).toString('base64url') + '.' + c.split('.')[1];// JWT: same idea — ALWAYS jwt.verify(token, key, { algorithms: ['RS256'] }); jwt.decode() is NOT verification.Verified by 4 asserts
assert.deepEqual(readCookie(c), { uid: 1, role: 'user' });assert.equal(readCookie(forged), null); // signature doesn't match tampered bodyassert.equal(readCookie(c.slice(0, -1) + (c.endsWith('A') ? 'B' : 'A')), null);assert.equal(readCookie('nodot'), null);Subresource Integrity for CDN scripts#
// VULNERABLE: <script src="https://cdn.example.com/lib.js"></script> // CDN compromise => XSS on every page// FIXED:function sriTag(src, bytes) { const hash = crypto.createHash('sha384').update(bytes).digest('base64'); return `<script src="${src}" integrity="sha384-${hash}" crossorigin="anonymous"></script>`;}const tag = sriTag('https://cdn.example.com/lib.js', Buffer.from('console.log(1)'));// Browser recomputes the hash of the fetched bytes; mismatch => script refused.Verified by 2 asserts
assert.match(tag, /integrity="sha384-[A-Za-z0-9+/=]{64}"/);assert.match(tag, /crossorigin="anonymous"/);Pipeline / update integrity (checklist)#
// Auto-update: fetch over TLS, verify detached signature (minisign/Sigstore cosign) before install.// CI: protected branches, required reviews, pipeline-as-code reviewed, no secrets exposed to fork PRs,// artifacts hashed + attested (SLSA provenance), deploy only artifacts built by CI (no laptop builds).// Webhooks: verify X-Hub-Signature-256 (HMAC of raw body) with timingSafeEqual before parsing.See also