Skip to content

content_marking: enforce subject, eat_profile, anchor, and strict UTF-8 / RFC 8259 JSON boundaries in verify_assertion and _loads #555

Closed

Description

@drk-ram

What is wrong

Contributor: Raghuvar Durga (@drk-ram)

On main (63e38c6), src/agentrust_trace/content_marking.py has two verification boundary gaps across build_assertion, verify_assertion, and _loads:

  1. Producer-consumer asymmetry in verify_assertion for subject, eat_profile, and anchor:
    • build_assertion (lines 173-204) enforces that anchor (when present) is a non-empty str, eat_profile is a non-empty str (content_marking.build_assertion accepts truthy non-string eat_profile values #399), and subject matches _SUBJECT_RE (spiffe://... or did:...). (Note also that build_assertion line 193 calls _SUBJECT_RE.match(str(subject or "")) without checking isinstance(subject, str) first.)
    • verify_assertion (lines 274-307), however, checks only field not in data and record.get(field) != data.get(field). It never validates that subject is a string matching _SUBJECT_RE, that eat_profile is a non-empty str, or that anchor (when present in data) is a non-empty str.
    • As a result, verify_assertion accepts malformed assertions whenever the assertion and record agree on the invalid value:
import hashlib, json
from agentrust_trace.content_marking import verify_assertion

raw_bad = json.dumps({"subject": 123, "eat_profile": "", "anchor": ""}).encode("utf-8")
bad_assertion = {
    "label": "com.agentrust-io.trace",
    "data": {
        "version": 1,
        "record": {
            "url": "https://example.com/r.json",
            "alg": "sha256",
            "hash": "sha256:" + hashlib.sha256(raw_bad).hexdigest(),
        },
        "subject": 123,
        "eat_profile": "",
        "anchor": "",
    },
}
verify_assertion(bad_assertion, raw_bad)
# Returns: {'subject': 123, 'eat_profile': '', 'anchor': ''}
  1. Non-UTF-8 BOM, non-RFC-8259 NaN/Infinity, and unpaired UTF-16 surrogates in _loads and string fields:
    • _loads(data, what) (lines 77-86) passes raw bytes directly to json.loads(data, object_pairs_hook=_no_duplicate_members) without decoding as strict UTF-8 (RFC 8259 §8.1, including rejecting a leading U+FEFF BOM) or passing parse_constant to reject NaN, Infinity, and -Infinity.
    • Furthermore, unpaired UTF-16 surrogates (U+D800 to U+DFFF) in record_bytes or assertion (such as anchor="\ud800", visible in tools/sweep_public_surface.py's ACCEPTED column for content_marking.build_assertion.anchor) are accepted by both build_assertion and verify_assertion:
import json
from agentrust_trace.content_marking import build_assertion, verify_assertion

utf16_bytes = json.dumps({
    "subject": "did:web:example.com",
    "eat_profile": "tag:agentrust-io.com,2026:trace-v0.2",
}).encode("utf-16")
a_utf16 = build_assertion(utf16_bytes, url="https://example.com/r.json")
verify_assertion(a_utf16, utf16_bytes)  # Succeeds on UTF-16 BOM bytes!

nan_bytes = b'{"subject":"did:web:example.com","eat_profile":"tag:agentrust-io.com,2026:trace-v0.2","iat":NaN}'
a_nan = build_assertion(nan_bytes, url="https://example.com/r.json")
verify_assertion(a_nan, nan_bytes)  # Succeeds on NaN!

Spec section or file

  • spec/content-marking-v1.md §2 and §4
  • src/agentrust_trace/content_marking.py (_loads, build_assertion, verify_assertion)

Expected behavior

  1. verify_assertion should validate subject (isinstance(subject, str) and _SUBJECT_RE.match(subject)), eat_profile (isinstance(profile, str) and profile), and anchor (when "anchor" in data, isinstance(anchor, str) and anchor with no unpaired UTF-16 surrogates) with ContentMarkingError before comparing against the record's fields, matching build_assertion.
  2. build_assertion should require isinstance(subject, str) before matching _SUBJECT_RE, and reject unpaired UTF-16 surrogates (U+D800 to U+DFFF) in string arguments and parsed structures.
  3. _loads should decode bytes strictly as UTF-8 (rejecting non-UTF-8 encodings and a leading U+FEFF byte-order mark), reject NaN/Infinity/-Infinity via parse_constant, and reject unpaired UTF-16 surrogates with ContentMarkingError.

Impact

Causes verify_assertion to accept C2PA assertions with non-URI/non-string subject, empty or non-string eat_profile, and empty or non-string anchor, and causes both build_assertion and verify_assertion to accept UTF-16/UTF-32 BOM-encoded payloads, NaN/Infinity constants, and unpaired UTF-16 surrogates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions