Skip to content

fix: Limit the maximum number of assertions allowed for C2PA Manifest#1951

Open
ssanthosh wants to merge 3 commits intomainfrom
ssanthosh/vulnerability_fixes_unbounded_assertions
Open

fix: Limit the maximum number of assertions allowed for C2PA Manifest#1951
ssanthosh wants to merge 3 commits intomainfrom
ssanthosh/vulnerability_fixes_unbounded_assertions

Conversation

@ssanthosh
Copy link

@ssanthosh ssanthosh commented Mar 18, 2026

Changes in this pull request

Security Fix: Unbounded Assertion Count Allows Resource Exhaustion

Issue

No limit exists on the number of assertions that can be added to a manifest across three distinct code paths:

  1. Builder::add_assertion / add_assertion_json (builder.rs) — the public API for constructing manifests programmatically.
  2. Claim::add_assertion (claim.rs) — the lower-level API callable directly, bypassing Builder.
  3. Store::from_jumbf_impl (store.rs) — the parsing path that processes assertions from untrusted, existing C2PA files.

An attacker can exploit paths 1 and 2 by constructing a manifest with an arbitrarily large number of assertions, and path 3 by supplying a crafted C2PA file with excessive assertions. All three cause uncontrolled memory and CPU consumption — a DoS vector for any application that creates or reads C2PA content.


Fix

New error variant (sdk/src/error.rs)

TooManyAssertions { max: usize }

Returned by all three enforcement points when the configured limit is exceeded.

New settings fields

  • BuilderSettings.max_assertions: usize (sdk/src/settings/builder.rs) — default 50. Controls assertion creation via Builder and Claim.
  • Verify.max_assertions: usize (sdk/src/settings/mod.rs) — default 50. Controls assertion parsing from untrusted files. Kept separate from builder.max_assertions so operators can tune creation and parsing limits independently.

Limit enforcement

File Where Reads
sdk/src/builder.rs add_assertion, add_assertion_json context.settings().builder.max_assertions
sdk/src/claim.rs add_assertion_impl get_thread_local_settings().builder.max_assertions
sdk/src/store.rs from_jumbf_impl, before the assertion parsing loop get_thread_local_settings().verify.max_assertions

Each point returns Err(TooManyAssertions { max }) before processing begins when the count meets or exceeds the limit.


Tests

builder.rstest_add_assertion_limit

  • Adds 50 assertions (default limit) — all succeed.
  • Verifies the 51st returns TooManyAssertions { max: 50 }.
  • Configurable limit: max_assertions: 2 via settings; verifies the 3rd is rejected.

claim.rstest_add_assertion_default_limit_in_claim

  • No settings manipulation; uses the default limit of 50.
  • Adds 50 assertions — all succeed; 51st returns TooManyAssertions { max: 50 }.

claim.rstest_add_assertion_limit_in_claim

  • Lowers builder.max_assertions to 2 via thread-local settings.
  • Adds 2 assertions — succeed; 3rd returns TooManyAssertions { max: 2 }.

store.rstest_verify_default_assertion_limit_in_store

  • Raises builder limit to 60 to create a manifest with 51 user assertions (signing adds further assertions).
  • Restores builder limit; parses with the default verify.max_assertions (50) untouched.
  • Verifies parse returns TooManyAssertions { max: 50 }.

store.rstest_verify_assertion_limit_in_store

  • Creates a manifest with 2 assertions; lowers verify.max_assertions to 1.
  • Verifies parse returns TooManyAssertions { max: 1 }.

Checklist

  • This PR represents a single feature, fix, or change.
  • All applicable changes have been documented.
  • Any TO DO items (or similar) have been entered as GitHub issues and the link to that issue has been included in a comment.

@ssanthosh ssanthosh self-assigned this Mar 18, 2026
@ssanthosh ssanthosh added bug Something isn't working safe to test labels Mar 18, 2026
@codspeed-hq
Copy link

codspeed-hq bot commented Mar 18, 2026

Merging this PR will not alter performance

✅ 16 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing ssanthosh/vulnerability_fixes_unbounded_assertions (264794a) with main (8efe78c)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@ssanthosh ssanthosh force-pushed the ssanthosh/vulnerability_fixes_unbounded_assertions branch from 8932506 to 8ded3b2 Compare March 19, 2026 06:47
Copy link
Contributor

@tmathern tmathern left a comment

Choose a reason for hiding this comment

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

Some APIs used are deprecated - local thread settings were replaced by settings on context. So some checks need to be weaved through differently.

/// of assertions. Calls to [`Builder::add_assertion`] or [`Builder::add_assertion_json`]
/// that would exceed this limit return [`Error::TooManyAssertions`].
///
/// The default value is 50.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why 50?

S: Into<String>,
T: Serialize,
{
let max_assertions = self.context.settings().builder.max_assertions;
Copy link
Contributor

Choose a reason for hiding this comment

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

Optional: If doing twice exactly the same check, maybe it could be refactored?


#[test]
fn test_add_assertion_limit() {
use crate::settings::Settings;
Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer all exports on top at the test suite

Copy link
Collaborator

Choose a reason for hiding this comment

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

or at the top of the test module in this case

assert!(matches!(err, Error::TooManyAssertions { max: 50 }));

// Verify the limit is configurable: set max_assertions=2 via settings.
let settings = Settings {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be another test, test_add_assertion_limit_is_configurable

) -> Result<C2PAAssertion> {
// Enforce the per-manifest assertion limit during signing to prevent
// resource exhaustion regardless of how the claim is constructed.
let max_assertions = get_thread_local_settings().builder.max_assertions;
Copy link
Contributor

Choose a reason for hiding this comment

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

I get the reason for this is that there is no context here or whatsoever to read the setting. But thread_local settings are another settings API that is deprecated. This likely needs to be weaved through differently.

Copy link
Collaborator

@gpeacock gpeacock Mar 19, 2026

Choose a reason for hiding this comment

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

If we are using a setting it should only be one from the context. Perhaps the answer is we just set a large fixed constant for this. If we really need to configure it in claim, then we may want to consider adding a way to set a max assertions value in claim from the current settings. But I think a constant may be fine. The other approach is to add context or settings to Claim but that feels like overkill for this.


// Reject manifests that embed more assertions than the configured limit to
// prevent unbounded memory and CPU consumption on untrusted input.
let max_assertions = get_thread_local_settings().verify.max_assertions;
Copy link
Contributor

Choose a reason for hiding this comment

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

I get the reason for this is that there is no context here or whatsoever to read the setting. But thread_local settings are another settings API that is deprecated. This likely needs to be weaved through differently.


#[test]
fn test_add_assertion_limit() {
use crate::settings::Settings;
Copy link
Collaborator

Choose a reason for hiding this comment

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

or at the top of the test module in this case

) -> Result<C2PAAssertion> {
// Enforce the per-manifest assertion limit during signing to prevent
// resource exhaustion regardless of how the claim is constructed.
let max_assertions = get_thread_local_settings().builder.max_assertions;
Copy link
Collaborator

@gpeacock gpeacock Mar 19, 2026

Choose a reason for hiding this comment

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

If we are using a setting it should only be one from the context. Perhaps the answer is we just set a large fixed constant for this. If we really need to configure it in claim, then we may want to consider adding a way to set a max assertions value in claim from the current settings. But I think a constant may be fine. The other approach is to add context or settings to Claim but that feels like overkill for this.

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

Labels

bug Something isn't working safe to test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants