| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { type BrokeredSecret, fill, misdirectedNote, newlyMisdirected, scrub, scrubHeaders, scrubbable, standInsIn } from "./broker.ts"; |
| 5 | |
| 6 | const NPM: BrokeredSecret = { |
| 7 | standIn: `g1tsec_${"a".repeat(48)}`, |
| 8 | name: "NPM_TOKEN", |
| 9 | value: "npm_realvalue123", |
| 10 | hosts: ["registry.npmjs.org"], |
| 11 | }; |
| 12 | const API: BrokeredSecret = { |
| 13 | standIn: `g1tsec_${"b".repeat(48)}`, |
| 14 | name: "API_KEY", |
| 15 | value: "key with spaces&=", |
| 16 | hosts: ["*.example.com"], |
| 17 | }; |
| 18 | const secrets = [NPM, API]; |
| 19 | |
| 20 | const basic = (text: string) => `Basic ${btoa(text)}`; |
| 21 | |
| 22 | test("stand-ins are found in the URL, the headers and Basic credentials", () => { |
| 23 | assert.deepEqual(standInsIn(`https://x.example.com/?key=${API.standIn}`, new Headers()), [API.standIn]); |
| 24 | assert.deepEqual(standInsIn("https://registry.npmjs.org/", new Headers({ authorization: `Bearer ${NPM.standIn}` })), [NPM.standIn]); |
| 25 | assert.deepEqual(standInsIn("https://registry.npmjs.org/", new Headers({ authorization: basic(`me:${NPM.standIn}`) })), [NPM.standIn]); |
| 26 | assert.deepEqual(standInsIn("https://registry.npmjs.org/", new Headers({ authorization: "Bearer g1tsec_short" })), []); |
| 27 | }); |
| 28 | |
| 29 | test("a stand-in in a header is filled in for its host", () => { |
| 30 | const headers = new Headers({ authorization: `Bearer ${NPM.standIn}`, "x-other": "kept" }); |
| 31 | const out = fill("https://registry.npmjs.org/pkg", headers, secrets); |
| 32 | assert.equal(out.headers.get("authorization"), "Bearer npm_realvalue123"); |
| 33 | assert.equal(out.headers.get("x-other"), "kept"); |
| 34 | assert.deepEqual(out.filled, [NPM]); |
| 35 | assert.deepEqual(out.misdirected, []); |
| 36 | }); |
| 37 | |
| 38 | test("custom headers and token schemes are filled in as well", () => { |
| 39 | const headers = new Headers({ authorization: `token ${API.standIn}`, "x-api-key": API.standIn }); |
| 40 | const out = fill("https://api.example.com/v1", headers, secrets); |
| 41 | assert.equal(out.headers.get("authorization"), `token ${API.value}`); |
| 42 | assert.equal(out.headers.get("x-api-key"), API.value); |
| 43 | }); |
| 44 | |
| 45 | test("Basic credentials are decoded, filled in and encoded again", () => { |
| 46 | const headers = new Headers({ authorization: basic(`me:${NPM.standIn}`) }); |
| 47 | const out = fill("https://registry.npmjs.org/", headers, secrets); |
| 48 | assert.equal(out.headers.get("authorization"), basic("me:npm_realvalue123")); |
| 49 | }); |
| 50 | |
| 51 | test("a stand-in in the query string is filled in, encoded", () => { |
| 52 | const out = fill(`https://api.example.com/v1?key=${API.standIn}&q=1`, new Headers(), secrets); |
| 53 | assert.equal(new URL(out.url).searchParams.get("key"), API.value); |
| 54 | assert.equal(new URL(out.url).searchParams.get("q"), "1"); |
| 55 | assert.ok(!out.url.includes(API.standIn)); |
| 56 | }); |
| 57 | |
| 58 | test("a stand-in sent to another host is left as it is and reported", () => { |
| 59 | const headers = new Headers({ authorization: `Bearer ${NPM.standIn}` }); |
| 60 | const out = fill("https://evil.com/steal", headers, secrets); |
| 61 | assert.equal(out.headers.get("authorization"), `Bearer ${NPM.standIn}`); |
| 62 | assert.deepEqual(out.filled, []); |
| 63 | assert.deepEqual(out.misdirected, [NPM]); |
| 64 | // Not even a host that only looks like it. |
| 65 | assert.deepEqual(fill("https://registry.npmjs.org.evil.com/", headers, secrets).filled, []); |
| 66 | assert.deepEqual(fill("https://example.com/", new Headers({ "x-api-key": API.standIn }), secrets).filled, []); |
| 67 | }); |
| 68 | |
| 69 | test("a value is filled in only over HTTPS, even for its own host", () => { |
| 70 | const headers = new Headers({ authorization: `Bearer ${NPM.standIn}` }); |
| 71 | const plain = fill("http://registry.npmjs.org/pkg", headers, secrets); |
| 72 | assert.equal(plain.headers.get("authorization"), `Bearer ${NPM.standIn}`); |
| 73 | assert.deepEqual(plain.filled, []); |
| 74 | assert.deepEqual(plain.misdirected, [NPM]); |
| 75 | }); |
| 76 | |
| 77 | test("a stand-in the job was not given is left alone, and not reported", () => { |
| 78 | const stranger = `g1tsec_${"c".repeat(48)}`; |
| 79 | const out = fill("https://registry.npmjs.org/", new Headers({ authorization: `Bearer ${stranger}` }), secrets); |
| 80 | assert.equal(out.headers.get("authorization"), `Bearer ${stranger}`); |
| 81 | assert.deepEqual(out.filled, []); |
| 82 | assert.deepEqual(out.misdirected, []); |
| 83 | }); |
| 84 | |
| 85 | test("a value echoed back is swapped for its stand-in again", () => { |
| 86 | const headers = scrubHeaders(new Headers({ "x-echo": "Bearer npm_realvalue123", "content-type": "application/json" }), [NPM]); |
| 87 | assert.equal(headers.get("x-echo"), `Bearer ${NPM.standIn}`); |
| 88 | assert.equal(scrub('{"token":"npm_realvalue123"}', [NPM]), `{"token":"${NPM.standIn}"}`); |
| 89 | }); |
| 90 | |
| 91 | test("only small, plain, uncompressed answers have their bodies rewritten", () => { |
| 92 | assert.equal(scrubbable(new Headers({ "content-type": "application/json", "content-length": "120" })), true); |
| 93 | assert.equal(scrubbable(new Headers({ "content-type": "text/plain; charset=utf-8", "content-length": "10" })), true); |
| 94 | assert.equal(scrubbable(new Headers({ "content-type": "application/vnd.api+json", "content-length": "10" })), true); |
| 95 | assert.equal(scrubbable(new Headers({ "content-type": "application/octet-stream", "content-length": "10" })), false); |
| 96 | assert.equal(scrubbable(new Headers({ "content-type": "application/json", "content-length": "10", "content-encoding": "gzip" })), false); |
| 97 | assert.equal(scrubbable(new Headers({ "content-type": "application/json" })), false); |
| 98 | assert.equal(scrubbable(new Headers({ "content-type": "application/json", "content-length": String(10 * 1024 * 1024) })), false); |
| 99 | }); |
| 100 | |
| 101 | test("a misdirected secret is noted once per host", () => { |
| 102 | const first = newlyMisdirected([], "NPM_TOKEN", "Evil.com:443"); |
| 103 | assert.ok(first); |
| 104 | assert.equal(first.note, misdirectedNote("NPM_TOKEN", "evil.com")); |
| 105 | assert.match(first.note, /^##\[warning\]NPM_TOKEN was sent to evil\.com, which it is not for/); |
| 106 | assert.equal(newlyMisdirected(first.seen, "NPM_TOKEN", "evil.com"), null); |
| 107 | assert.ok(newlyMisdirected(first.seen, "NPM_TOKEN", "other.com")); |
| 108 | }); |