Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
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.

saml-signature.test.tsBlame82 lines · 1 contributor
45ef109ccantynz-alt1/**
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
21import { describe, expect, it } from "bun:test";
22import { readFileSync } from "fs";
23
24const SRC = readFileSync("src/routes/saml-sso.tsx", "utf8");
25
26describe("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
38describe("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
63describe("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
77describe("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});