fix(core-consent): validate migrated categories against an allow-list
The anonymous consent cookie is client-controlled, yet its categories were granted verbatim at sign-up migration (audit finding A12; the migration itself is already invoked in the auth sign-up use case and bindAllProduction now threads a consentFactory so it runs in production). Adds KNOWN_CONSENT_CATEGORIES + isKnownConsentCategory to core-consent, filters in extractAnonymousConsent and migrateAnonymousConsent, and mirrors the allow-list in the auth sign-up cookie extractor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,43 @@ describe("signUpUseCase", () => {
|
||||
expect(result.clearCookie?.attributes.maxAge).toBe(0);
|
||||
});
|
||||
|
||||
it("drops unknown categories from the client-controlled cookie (A12)", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const consent = new RecordingConsent();
|
||||
const consentFactory = (_userId: string) => Promise.resolve(consent);
|
||||
const useCase = signUpUseCase(users, auth, bus, consentFactory);
|
||||
|
||||
await useCase({
|
||||
username: "ivy",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
cookieHeader: "cc_consent=analytics,evil-made-up,__proto__; session=x",
|
||||
});
|
||||
|
||||
expect(consent.grants.map((g) => g.category)).toEqual(["analytics"]);
|
||||
});
|
||||
|
||||
it("does not migrate consent when every cookie category is unknown (A12)", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
const bus = new RecordingEventBus();
|
||||
const consent = new RecordingConsent();
|
||||
const consentFactory = (_userId: string) => Promise.resolve(consent);
|
||||
const useCase = signUpUseCase(users, auth, bus, consentFactory);
|
||||
|
||||
const result = await useCase({
|
||||
username: "jack",
|
||||
password: "secret_password",
|
||||
confirmPassword: "secret_password",
|
||||
cookieHeader: "cc_consent=hax,not-a-category",
|
||||
});
|
||||
|
||||
expect(consent.grants).toHaveLength(0);
|
||||
expect(result.clearCookie).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not migrate consent when no cc_consent cookie is present", async () => {
|
||||
const users = new MockUsersRepository([]);
|
||||
const auth = new MockAuthenticationService(users);
|
||||
|
||||
@@ -14,6 +14,16 @@ import type { IAuthenticationService } from "../services/authentication.service.
|
||||
// Cookie name written by the anonymous consent banner (mirrors CONSENT_COOKIE_NAME in @repo/core-consent).
|
||||
const ANONYMOUS_CONSENT_COOKIE = "cc_consent";
|
||||
|
||||
// Category allow-list (mirrors KNOWN_CONSENT_CATEGORIES in @repo/core-consent).
|
||||
// The cookie is client-controlled: unknown strings are dropped, never granted
|
||||
// (audit finding A12).
|
||||
const KNOWN_CONSENT_CATEGORIES = [
|
||||
"necessary",
|
||||
"functional",
|
||||
"analytics",
|
||||
"marketing",
|
||||
];
|
||||
|
||||
function extractConsentFromCookieHeader(cookieHeader: string): string[] | null {
|
||||
for (const part of cookieHeader.split(";")) {
|
||||
const eqIdx = part.indexOf("=");
|
||||
@@ -24,7 +34,8 @@ function extractConsentFromCookieHeader(cookieHeader: string): string[] | null {
|
||||
const cats = value
|
||||
.split(",")
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean);
|
||||
.filter(Boolean)
|
||||
.filter((c) => KNOWN_CONSENT_CATEGORIES.includes(c));
|
||||
return cats.length > 0 ? cats : null;
|
||||
}
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user