Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions examples/protect/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# @patchstack/connect/protect — end-to-end demo

Shows the full **Verified Vulnerability Shielding** loop against a **real, unmodified
vulnerable dependency** — no mocks of the vulnerability itself.

```bash
cd examples/protect
npm install # pulls the real vulnerable lodash@4.17.11 (CVE-2019-10744)
npm run demo
```

Expected: all six steps ✓.

## What it demonstrates

| Step | |
|---|---|
| 1 | The exploit works **unprotected** — `lodash.defaultsDeep` on a `{"constructor":{"prototype":…}}` body pollutes `Object.prototype`. |
| 2 | **dry-run**: the vPatch *detects + logs* the exploit but still serves it (the safe onramp). |
| 3 | **block**: the request is rejected (403) before the vulnerable sink runs — prototype stays clean. |
| 4 | A **benign** request to the same route is still served (no false positive). |
| 5 | A response that accidentally **leaks an AWS key** has it **redacted** (`[REDACTED]`) while the page is still served. |
| 6 | An **outbound SSRF** to cloud metadata (`169.254.169.254`) is blocked; an external call is allowed. |

…and prints the proof line: *"CVE-2019-10744 in lodash@4.17.11 is blocked here, right now,
by rule `demo-CVE-2019-10744` — until you upgrade to 4.17.12. No app redeploy required."*

## Note on rules

`rules.demo.json` holds **example rules for public CVEs only** — it is **not** the
Patchstack production corpus. In a real deployment the per-site, version-scoped rule set is
fetched from the Patchstack API (`createProtection({ token })`), cached to disk. The demo
uses a local bundle and **no token / secret**.
94 changes: 94 additions & 0 deletions examples/protect/demo.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// End-to-end "Verified Vulnerability Shielding" demo for @patchstack/connect/protect.
//
// A REAL, unmodified vulnerable dependency (lodash@4.17.11, CVE-2019-10744) is exploited
// live through an app endpoint; then the vPatch is applied and the SAME exploit is replayed
// and blocked — with an auditable proof. Also demonstrates response secret-leak redaction
// and egress SSRF blocking. Public CVE + demo rules only; no tokens/secrets.
//
// cd examples/protect && npm install && node demo.mjs
import { readFileSync } from 'node:fs';
import _ from 'lodash';
import { createProtection } from '../../src/protect/runtime.js';

const rules = JSON.parse(readFileSync(new URL('./rules.demo.json', import.meta.url), 'utf8'));
const LODASH = _.VERSION; // 4.17.11 (vulnerable; fixed in 4.17.12)

let ok = true;
const line = (pass, msg) => { ok = pass && ok; console.log(` ${pass ? '✓' : '✗'} ${msg}`); };

// The vulnerable app endpoint: "save settings" deep-merges the JSON body via lodash — the
// CVE-2019-10744 sink.
const appHandler = async (request) => {
const body = await request.json().catch(() => ({}));
_.defaultsDeep({}, body); // vulnerable sink
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' } });
};

const exploit = () =>
new Request('https://app.demo/api/settings', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{"constructor":{"prototype":{"polluted":"yes"}}}',
});

const polluted = () => {
const hit = ({}).polluted === 'yes';
delete Object.prototype.polluted;
return hit;
};

console.log(`\nTarget: lodash@${LODASH} (CVE-2019-10744, fixed in 4.17.12)\n`);

// 1. UNPROTECTED — the exploit works.
await appHandler(exploit());
line(polluted(), '1. unprotected: exploit pollutes Object.prototype (VULNERABLE)');

// 2. DRY-RUN — detected, logged, NOT enforced (the safe onramp).
{
const detections = [];
const p = await createProtection({ rules, mode: 'dry-run', onDetect: (d) => detections.push(d) });
await p.fetch(appHandler)(exploit());
const detected = detections.some((d) => d.rule?.id === 'demo-CVE-2019-10744');
line(detected && polluted(), '2. dry-run: detected + logged, but still served (not enforced)');
}

// 3. BLOCK — request rejected before the sink runs; no pollution.
{
const p = await createProtection({ rules, mode: 'block' });
const res = await p.fetch(appHandler)(exploit());
line(res.status === 403 && !polluted(), '3. block: exploit → 403, sink never runs, prototype clean');
const benign = await p.fetch(appHandler)(new Request('https://app.demo/api/settings', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{"theme":"dark"}' }));
line(benign.status === 200, '4. block: benign request still served (200, no false positive)');
}

// 5. RESPONSE leak — an endpoint accidentally returns a key; it's masked, page still served.
{
const p = await createProtection({ mode: 'block' }); // default response rules
const leak = () => new Response(JSON.stringify({ ok: true, awsKey: 'AKIAIOSFODNN7EXAMPLE' }), { status: 200, headers: { 'content-type': 'application/json' } });
const res = await p.fetch(leak)(new Request('https://app.demo/config'));
const body = await res.text();
line(res.status === 200 && !body.includes('AKIAIOSFODNN7EXAMPLE') && body.includes('[REDACTED]'), '5. response: leaked AWS key redacted, page still served');
}

// 6. EGRESS SSRF — the app's outbound call to cloud metadata is blocked (stubbed fetch).
{
const orig = globalThis.fetch;
globalThis.fetch = async (u) => ({ marker: 'stub', url: String(u) });
const p = await createProtection({ egress: true, mode: 'block' });
try {
let blocked = false;
try { await globalThis.fetch('http://169.254.169.254/latest/meta-data/'); } catch { blocked = true; }
const ext = await globalThis.fetch('https://api.github.com/');
line(blocked && ext.marker === 'stub', '6. egress: outbound to 169.254.169.254 blocked, external allowed');
} finally {
p.uninstallEgress?.();
globalThis.fetch = orig;
}
}

console.log(
`\n PROOF: CVE-2019-10744 in lodash@${LODASH} is blocked here, right now, by rule ` +
`demo-CVE-2019-10744 — until you upgrade to 4.17.12. No app redeploy required.\n`,
);
console.log(ok ? '✓ ALL PASS\n' : '✗ FAILED\n');
process.exit(ok ? 0 : 1);
12 changes: 12 additions & 0 deletions examples/protect/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "connect-protect-demo",
"private": true,
"type": "module",
"description": "End-to-end demo for @patchstack/connect/protect (public CVE, demo rules only)",
"dependencies": {
"lodash": "4.17.11"
},
"scripts": {
"demo": "node demo.mjs"
}
}
45 changes: 45 additions & 0 deletions examples/protect/rules.demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"_comment": "DEMO / EXAMPLE rules for public CVEs only — NOT the Patchstack production corpus. In a real deployment the per-site, version-scoped rule set is fetched from the Patchstack API (createProtection({ token })). Hand-derived from public advisories for the example below.",
"firewall": [
{
"id": "demo-CVE-2019-10744",
"title": "Prototype pollution in lodash (defaultsDeep / merge / set)",
"vulnerability_id": "CVE-2019-10744",
"category": "prototype-pollution",
"rule_v2": [
{ "parameter": "raw", "mutations": ["urldecode"], "match": { "type": "contains", "value": "__proto__" } },
{
"parameter": "rules",
"rules": [
{ "parameter": "raw", "mutations": ["urldecode"], "match": { "type": "contains", "value": "constructor" }, "inclusive": true },
{ "parameter": "raw", "mutations": ["urldecode"], "match": { "type": "contains", "value": "prototype" }, "inclusive": true }
]
}
]
},
{
"id": "demo-path-traversal",
"title": "Path traversal in a file/path parameter",
"category": "lfi",
"rule_v2": [
{ "parameter": ["get.file", "post.file", "raw.file", "get.path", "post.path"], "mutations": ["urldecode"], "match": { "type": "contains", "value": ".." } }
]
},
{
"id": "demo-ssrf-url-param",
"title": "SSRF via a url parameter (request-side)",
"category": "ssrf",
"rule_v2": [
{
"parameter": "rules",
"rules": [
{ "parameter": ["get.url", "post.url", "raw.url"], "mutations": ["urldecode"], "match": { "type": "contains", "value": "localhost" } },
{ "parameter": ["get.url", "post.url", "raw.url"], "mutations": ["urldecode"], "match": { "type": "regex", "value": "/(127\\.0\\.0\\.1|169\\.254\\.169\\.254|::1|metadata\\.google)/i" } }
]
}
]
}
],
"whitelists": [],
"whitelist_keys": {}
}
Loading