Consent decisions recorded with timestamp, version, categories
Why it matters
GDPR Art. 7(1) places the burden of proof on the controller to demonstrate that consent was obtained. Art. 5(2) (accountability) reinforces this: the controller must be able to show, on request, that consent was lawful. A stored boolean 'true' fails this standard — it records that something was accepted, not when, under what banner version, or for which specific purposes. Recital 42 notes that consent should include the time of consent and information about what was consented to. Without a timestamped, structured record, the controller cannot demonstrate compliance to a DPA or defend against a subject access request.
Severity rationale
Medium because absent timestamp and version metadata makes it impossible to demonstrate lawful consent under GDPR Art. 7(1) and Art. 5(2), but the underlying processing may still be lawful if consent was genuinely given.
Remediation
Store a structured consent record that includes per-category decisions, timestamp, version, and the method of consent. This record supports both re-consent logic and regulatory accountability.
// src/lib/consent.ts
export type ConsentRecord = {
necessary: true
analytics: boolean
marketing: boolean
timestamp: string // ISO 8601
version: string
method: 'accept_all' | 'reject_all' | 'custom'
}
// On Accept All:
saveConsent({ analytics: true, marketing: true }, 'accept_all')
// On Reject All:
saveConsent({ analytics: false, marketing: false }, 'reject_all')
// On custom save:
saveConsent({ analytics, marketing }, 'custom')
For authenticated users, send this record to your server (a consent_records table) so you can produce an audit trail across devices and sessions if a regulator requests it.
Detection
-
ID:
consent-records-timestamped -
Severity:
medium -
What to look for: Inspect the stored consent object structure. Find where
localStorage.setItem(or the equivalent cookie set) is called in the consent save handler. Check the shape of the stored value — does it include: (1) per-category booleans (analytics, marketing, etc.), (2) a timestamp (ISO 8601 string of when the user made their choice), (3) a consent version (to trigger re-consent when cookies change), (4) optionally the user's decision method ("accept_all", "reject_all", "custom"). Regulators (GDPR Article 7, Recital 42) require that the controller be able to demonstrate that the data subject consented — having a timestamped record is part of that demonstration. Check whether the stored consent can be retrieved and displayed to the user (e.g., in a cookie preferences page showing "You last consented on [date]"). -
Pass criteria: Count all consent record fields stored. The stored consent object includes at minimum: per-category decisions (not just a single boolean), a timestamp, and a version. Ideally also the consent method (accept_all vs. custom). The format is parseable and the timestamp is a valid ISO 8601 string. At least 3 fields must be recorded: timestamp, consent version, and user choices.
-
Fail criteria: Consent stored as a single boolean (
localStorage.setItem('consented', 'true')). No timestamp in the stored object. No per-category breakdown in the stored record — just "accepted" or "declined" globally. -
Skip (N/A) when: No consent banner exists (already failing at
banner-first-visit). -
Detail on fail: Example:
"Consent stored as a single string 'true' at key 'cookieConsent'. No per-category breakdown, no timestamp, no version."or"Consent object has {analytics: true} but no timestamp or version field.". -
Remediation: Store a structured consent record. The
lib/consent.tsimplementation in theconsent-state-persistedremediation already includestimestampandversion. Additionally capture the consent method:export type ConsentRecord = { necessary: true analytics: boolean marketing: boolean timestamp: string // ISO 8601 — when user consented version: string // banner version at time of consent method: 'accept_all' | 'reject_all' | 'custom' // how user chose } // When user clicks "Accept All": saveConsent({ analytics: true, marketing: true }, 'accept_all') // When user clicks "Reject All": saveConsent({ analytics: false, marketing: false }, 'reject_all') // When user saves custom preferences: saveConsent({ analytics, marketing }, 'custom')This record can also be sent to your server for authenticated users, providing an audit trail demonstrating that consent was obtained.
External references
- gdpr · Art. 7(1) — GDPR — controller must be able to demonstrate consent
- gdpr · Recital 42 — GDPR — consent records must include timestamp and scope of consent
- gdpr · Art. 5(2) — GDPR — accountability principle; records of processing activities
Taxons
History
- 2026-04-18·v1.0.0·Initial import from cookie-consent-compliance·automated