Under GDPR Art. 7(1), the controller must be able to demonstrate that consent was obtained — which presupposes that the consent decision is durably stored. If consent is written to sessionStorage instead of localStorage or a first-party cookie, the banner reappears on every new browser session, which undermines CCPA §1798.135's opt-out persistence requirement and forces users to re-engage with the consent mechanism repeatedly. ePrivacy Art. 5(3) additionally requires that access to stored information only occur after consent is obtained — storing consent in a mechanism that doesn't survive the session means scripts fire on the very next load.
High because session-only consent storage causes the consent mechanism to functionally reset on every new tab or browser restart, making sustained lawful processing impossible under GDPR Art. 7 and ePrivacy Art. 5(3).
Write consent to localStorage (or a first-party cookie with a multi-year max-age) and include a version field to support re-consent when the cookie inventory changes. The initialization useEffect must read this value and only show the banner when nothing is stored or the version has changed.
// src/lib/consent.ts
const CONSENT_KEY = 'cookie_consent'
const CONSENT_VERSION = '1.0' // bump to trigger re-consent
export type ConsentState = {
necessary: true
analytics: boolean
marketing: boolean
timestamp: string
version: string
}
export function needsConsent(): boolean {
if (typeof window === 'undefined') return false
const raw = localStorage.getItem(CONSENT_KEY)
if (!raw) return true
try {
const stored = JSON.parse(raw) as ConsentState
return stored.version !== CONSENT_VERSION
} catch { return true }
}
export function saveConsent(state: Omit<ConsentState, 'necessary' | 'timestamp' | 'version'>): void {
localStorage.setItem(CONSENT_KEY, JSON.stringify({
necessary: true, ...state,
timestamp: new Date().toISOString(),
version: CONSENT_VERSION,
}))
}
Call needsConsent() in the banner's useEffect. Never use sessionStorage as the sole storage mechanism.
ID: cookie-consent-compliance.consent-enforcement.consent-state-persisted
Severity: high
What to look for: Check what happens when the banner is submitted. Find the save/accept/reject handler in the consent component. Verify that the consent decision is written to a persistent storage mechanism — localStorage (survives across sessions), a first-party cookie (also acceptable), or a server-side database (for authenticated users). Do NOT accept sessionStorage alone — session storage clears when the tab closes, so the banner would reappear on every new session. Then verify that on page load (the useEffect / onMounted initialization), the stored consent is read and used to both (a) suppress the banner and (b) load the appropriate scripts. Check that the stored value includes a timestamp and a version/hash field (needed for re-consent logic). Check whether authenticated users have their consent stored server-side and synced — if so, verify the sync logic.
Pass criteria: Count all consent storage mechanisms (cookies, localStorage, sessionStorage). Consent decision is written to localStorage or a persistent first-party cookie on save. On subsequent page loads, the stored consent is read and the banner does not reappear. The stored value includes at minimum: per-category decision, timestamp, and a version or hash field. If authenticated, consent state is consistent across devices. At least 1 persistent storage mechanism must retain the consent state across page loads.
Fail criteria: Consent stored in sessionStorage only — banner reappears on every new tab or session. Nothing is stored — banner reappears on every visit. Consent is stored but the initialization code does not read it (banner always shows). Consent decision not forwarded to script loading logic (scripts always load regardless of stored state).
Skip (N/A) when: No consent banner exists (already failing at banner-first-visit).
Detail on fail: Specify the persistence issue. Example: "Consent stored in sessionStorage only (key: 'consent'). Banner reappears every new browser session." or "Consent written to localStorage but initialization useEffect does not read localStorage — banner always displays and scripts always load regardless of stored preference.".
Remediation: Persist consent in localStorage with a structured object:
// lib/consent.ts
export type ConsentState = {
necessary: true
analytics: boolean
marketing: boolean
timestamp: string
version: string // bump when you add new third parties or categories
}
const CONSENT_KEY = 'cookie_consent'
const CONSENT_VERSION = '1.0' // bump this to trigger re-consent
export function getStoredConsent(): ConsentState | null {
if (typeof window === 'undefined') return null
const raw = localStorage.getItem(CONSENT_KEY)
if (!raw) return null
try {
return JSON.parse(raw) as ConsentState
} catch {
return null
}
}
export function saveConsent(state: Omit<ConsentState, 'necessary' | 'timestamp' | 'version'>): void {
const record: ConsentState = {
necessary: true,
...state,
timestamp: new Date().toISOString(),
version: CONSENT_VERSION,
}
localStorage.setItem(CONSENT_KEY, JSON.stringify(record))
}
export function needsConsent(): boolean {
const stored = getStoredConsent()
return !stored || stored.version !== CONSENT_VERSION
}
In the banner's useEffect: call needsConsent() — if true, show the banner. If false, call loadApprovedScripts(getStoredConsent()) to restore the previously consented scripts.