Provenance fields are immutable after creation
Why it matters
Provenance fields that can be overwritten are not provenance — they are annotations. If an enrichment job can silently replace source_id or acquired_at on an existing contact, the audit trail is broken. GDPR Art. 5(1)(d) requires data accuracy, which includes accuracy about the origin of the data. SLSA Provenance L2 requires that provenance records be unforgeable after creation. CWE-345 (Insufficient Verification of Data Authenticity) applies when records can be modified post-creation in ways that destroy traceability.
Severity rationale
High because mutable provenance fields allow enrichment jobs or admin tools to silently rewrite the record of origin, breaking GDPR Art. 30 compliance and destroying the evidentiary value of the audit trail.
Remediation
Exclude provenance fields from all update schemas at the application layer, and optionally enforce immutability at the database layer with a trigger.
// Zod update schema in src/lib/contacts/update.ts
const updateContactSchema = z.object({
email: z.string().email().optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
// source_type, source_id, acquired_at intentionally excluded
})
-- Database-level enforcement
CREATE OR REPLACE FUNCTION prevent_provenance_update()
RETURNS TRIGGER LANGUAGE plpgsql AS $$
BEGIN
IF NEW.source_type IS DISTINCT FROM OLD.source_type OR
NEW.source_id IS DISTINCT FROM OLD.source_id OR
NEW.acquired_at IS DISTINCT FROM OLD.acquired_at THEN
RAISE EXCEPTION 'Provenance fields are immutable after creation';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER enforce_provenance_immutability
BEFORE UPDATE ON contacts
FOR EACH ROW EXECUTE FUNCTION prevent_provenance_update();
Detection
-
ID:
provenance-immutable -
Severity:
high -
What to look for: Enumerate all UPDATE statements or ORM update calls on the contacts table. Count every update path and for each, check whether provenance fields (
source_type,source_id,acquired_at) are in the write set. Quote the actual update schema or Zod validation found. Check whether the contact update API endpoint, background enrichment jobs, or admin tools allow overwriting provenance data. Also check database triggers or column-level permissions that might enforce immutability at the database level. An update path that includes provenance fields does not count as pass. -
Pass criteria: Count all update paths and report the ratio: "0 of N update paths modify provenance fields." No application code path issues UPDATE statements that modify
source_type,source_id, oracquired_aton existing contact records. Ideally enforced at the database level via a trigger or generated-always column, but application-level enforcement is acceptable. -
Fail criteria: At least 1 enrichment job, update endpoint, or admin tool can overwrite provenance fields on existing records. The ORM update schema includes provenance fields in updatable fields.
-
Skip (N/A) when: The system has only one data source and provenance fields were not implemented (would already fail the required-provenance-fields check).
-
Detail on fail:
"Contact update endpoint includes source_type in updatable fields — provenance can be overwritten"or"Enrichment worker overwrites acquired_at when it re-processes a record". -
Remediation: Remove provenance fields from all update schemas, and optionally enforce immutability in the database:
// Zod update schema — provenance fields excluded const updateContactSchema = z.object({ email: z.string().email().optional(), first_name: z.string().optional(), // source_type, source_id, acquired_at intentionally omitted })-- Trigger to block provenance overwrite CREATE OR REPLACE FUNCTION prevent_provenance_update() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF NEW.source_type IS DISTINCT FROM OLD.source_type OR NEW.source_id IS DISTINCT FROM OLD.source_id OR NEW.acquired_at IS DISTINCT FROM OLD.acquired_at THEN RAISE EXCEPTION 'Provenance fields are immutable after creation'; END IF; RETURN NEW; END; $$; CREATE TRIGGER enforce_provenance_immutability BEFORE UPDATE ON contacts FOR EACH ROW EXECUTE FUNCTION prevent_provenance_update();
External references
- cwe · CWE-345 — Insufficient Verification of Data Authenticity
- slsa:1.0 · Provenance L2 — Provenance is authenticated and tamper-evident
- gdpr · Art. 5(1)(d) — Accuracy — provenance modification could invalidate lawful basis record
- gdpr · Art. 30 — Records of processing activities
Taxons
History
- 2026-04-18·v1.0.0·Initial import from data-sourcing-provenance·automated