Blame · Line-by-line history
saml-signature.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 45ef109 | 1 | /** |
| 2 | * SAML assertion verification. | |
| 3 | * | |
| 4 | * Two defects, both allowing account takeover: | |
| 5 | * | |
| 6 | * 1. Fail-open. Signature verification ran only `if (idpCertPem && trim())`, | |
| 7 | * so an org whose SAML config had a blank idp_certificate accepted ANY | |
| 8 | * forged SAMLResponse. The assertion's email selects the account to log | |
| 9 | * in as, so that is unauthenticated takeover of any account in the org. | |
| 10 | * | |
| 11 | * 2. Signature wrapping. verifySamlSignature checks only that SignedInfo | |
| 12 | * carries a valid RSA signature; the Reference DigestValue is never | |
| 13 | * recomputed. A genuinely-signed assertion could be edited — or a second | |
| 14 | * forged Assertion added beside the signed one — and still verify. | |
| 15 | * | |
| 16 | * Full XML-DSig needs exclusive c14n and a vetted library. These lock in the | |
| 17 | * fail-closed behaviour and the structural anti-wrapping checks that stand in | |
| 18 | * until then. | |
| 19 | */ | |
| 20 | ||
| 21 | import { describe, expect, it } from "bun:test"; | |
| 22 | import { readFileSync } from "fs"; | |
| 23 | ||
| 24 | const SRC = readFileSync("src/routes/saml-sso.tsx", "utf8"); | |
| 25 | ||
| 26 | describe("verification is mandatory (fail-closed)", () => { | |
| 27 | it("refuses when no IdP certificate is configured", () => { | |
| 28 | expect(SRC).toContain("if (!idpCertPem || !idpCertPem.trim())"); | |
| 29 | expect(SRC).toContain("Refusing to accept an unverified assertion"); | |
| 30 | }); | |
| 31 | ||
| 32 | it("no longer makes verification conditional on the cert being present", () => { | |
| 33 | // The original bug shape. | |
| 34 | expect(SRC).not.toMatch(/if \(idpCertPem && idpCertPem\.trim\(\)\) \{\s*\n\s*const sigValid/); | |
| 35 | }); | |
| 36 | }); | |
| 37 | ||
| 38 | describe("anti-wrapping structural checks", () => { | |
| 39 | it("runs before signature verification", () => { | |
| 40 | const coverage = SRC.indexOf("assertSignatureCoversAssertion(xml)"); | |
| 41 | const verify = SRC.indexOf("if (!verifySamlSignature(xml, idpCertPem))"); | |
| 42 | expect(coverage).toBeGreaterThan(-1); | |
| 43 | expect(verify).toBeGreaterThan(-1); | |
| 44 | // A rewritten-but-validly-signed document must not reach attribute | |
| 45 | // extraction just because SignedInfo checks out. | |
| 46 | expect(coverage).toBeLessThan(verify); | |
| 47 | }); | |
| 48 | ||
| 49 | it("rejects multiple Assertions", () => { | |
| 50 | expect(SRC).toContain("assertionIds.length > 1"); | |
| 51 | expect(SRC).toContain("signature-wrapping attempt"); | |
| 52 | }); | |
| 53 | ||
| 54 | it("rejects a Reference that covers neither the Response nor the Assertion", () => { | |
| 55 | expect(SRC).toContain("referenced !== assertionIds[0] && referenced !== responseId"); | |
| 56 | }); | |
| 57 | ||
| 58 | it("rejects a response with no Assertion at all", () => { | |
| 59 | expect(SRC).toContain("assertionIds.length === 0"); | |
| 60 | }); | |
| 61 | }); | |
| 62 | ||
| 63 | describe("signature algorithm selection", () => { | |
| 64 | it("reads Algorithm from SignatureMethod, not the first match in the doc", () => { | |
| 65 | // The old regex /Algorithm="([^"]+)"/ matched CanonicalizationMethod, | |
| 66 | // which appears first in a normal response — so the real algorithm was | |
| 67 | // never read and it silently fell back to SHA1. | |
| 68 | expect(SRC).toContain("SignatureMethod[^>]*\\bAlgorithm="); | |
| 69 | expect(SRC).not.toMatch(/const algMatch = xml\.match\(\/Algorithm=/); | |
| 70 | }); | |
| 71 | ||
| 72 | it("refuses an unrecognised algorithm instead of defaulting to SHA1", () => { | |
| 73 | expect(SRC).toContain("unrecognised SignatureMethod"); | |
| 74 | }); | |
| 75 | }); | |
| 76 | ||
| 77 | describe("the incompleteness is documented, not hidden", () => { | |
| 78 | it("verifySamlSignature is marked as not proving assertion integrity", () => { | |
| 79 | // Whoever reads this next must not mistake it for full XML-DSig. | |
| 80 | expect(SRC).toContain("INCOMPLETE BY DESIGN"); | |
| 81 | }); | |
| 82 | }); |