Skip to content

fix(content-marking): validate record.url external references - #284

Open
altrudev wants to merge 7 commits into
agentrust-io:mainfrom
altrudev:fix/content-marking-record-url-283
Open

fix(content-marking): validate record.url external references#284
altrudev wants to merge 7 commits into
agentrust-io:mainfrom
altrudev:fix/content-marking-record-url-283

Conversation

@altrudev

@altrudev altrudev commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Closes #283.

What

spec/content-marking-v1.md says the assertion's record object follows C2PA's external-reference shape, but the reference implementation previously treated record.url as truthiness only in both producer and consumer paths.

That allowed truthy non-string values and malformed/non-HTTP references to be emitted or accepted.

This change adds one shared _record_url() boundary and uses it from both build_assertion() and verify_assertion().

The boundary requires:

  • a string value;
  • no whitespace;
  • an RFC 3986-valid URI;
  • http or https scheme;
  • a non-empty authority/host component;
  • a syntactically valid port in the 0-65535 range when present.

It deliberately does not add broader URL policy beyond the external-reference boundary.

Regression coverage

Positive controls:

  • absolute HTTPS URL;
  • absolute HTTP URL.

Refusals on both producer and verifier paths:

  • boolean;
  • integer;
  • list;
  • object;
  • whitespace-only string;
  • arbitrary text;
  • FTP URL;
  • HTTP(S) URL with no authority;
  • malformed authority with no hostname;
  • non-numeric port.

A separate mutation-oriented test records the original failure class: several malformed values are truthy, so restoring the old if not url / if not ref.get("url") checks would reopen the boundary.

DDC Radial review

The first candidate used urllib.parse.urlsplit(). The repository's offline invariant deliberately forbids importing network-adjacent standard-library modules, so CI correctly rejected that approach even though the focused URL tests passed. The implementation was replaced with the repository's already-installed rfc3986-validator plus a narrow authority/port boundary.

A second CI pass then caught that rfc3986-validator does not ship typing metadata. The final import carries a narrowly scoped # type: ignore[import-untyped]; mypy is not weakened globally.

The final exact head e15651d9f6a23c04f2d07e111563b9e76e36bd6e passed both repository CI and CodeQL (CI run 607, CodeQL run 407).

Scope

Content-marking producer/consumer validation only. No change to hashing, Trust Record verification, C2PA signature handling, or TRACE wire format.

AI-assistance disclosure: ChatGPT assisted with source triage, adversarial-case design, implementation drafting, CI failure analysis, and diff review. altrudev reviewed the bounded claim and remains responsible for the contribution.

@altrudev
altrudev requested a review from a team as a code owner September 4, 2026 13:24
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🔴 Contributor Check: HIGH

Check Result
Profile HIGH
Credential LOW
Overall HIGH

Automated check by AgenTrust Contributor Check.

@github-actions github-actions Bot added the needs-review:HIGH Contributor check flagged HIGH risk label Sep 4, 2026

@lywinged lywinged left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved on e15651d over fc38496. Reproduced on main: build_assertion(url=True), and verify_assertion() with record.url set to true or to an ftp:// value, all pass. On this head all three are refused and the two positive controls pass; the PR's own test file has 20 of 23 cases failing against main and none here. Beyond the PR's matrix I put 24 more inputs through _record_url(): it accepts the RFC 3986 forms, upper-case scheme, userinfo, IPv6 literals, ports 0 to 65535, a trailing-dot host, and refuses port 65536, an empty port, whitespace and %zz. One behaviour the body does not name: a non-ASCII host such as https://例子.测试/r is refused while its punycode form is accepted. That is the right call for a C2PA URI, and it means a producer with an IDN host now has to encode it, so it belongs in the body. rfc3986_validator is already a declared dependency. Merges cleanly with #282; the combined tree passes 1,197. Suite 1,190 passed, 1 skipped; ruff, mypy and the dash check clean.

Tool-assisted: the runs and this write-up.

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

Labels

needs-review:HIGH Contributor check flagged HIGH risk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

content-marking: record.url accepts values outside the C2PA external-reference shape

2 participants