From 16317fcb3ef75082992a7cd4bd480a7076ea4a6a Mon Sep 17 00:00:00 2001 From: ale-adobe Date: Thu, 14 May 2026 15:14:31 -0700 Subject: [PATCH 1/3] Add checks for valid trust keys, response size, and URL fetch limits --- packages/c2pa-web/src/lib/settings.spec.ts | 80 +++++++++++++++++++++- packages/c2pa-web/src/lib/settings.ts | 30 ++++++-- 2 files changed, 105 insertions(+), 5 deletions(-) diff --git a/packages/c2pa-web/src/lib/settings.spec.ts b/packages/c2pa-web/src/lib/settings.spec.ts index 47c131f..8d29eb5 100644 --- a/packages/c2pa-web/src/lib/settings.spec.ts +++ b/packages/c2pa-web/src/lib/settings.spec.ts @@ -9,7 +9,7 @@ import { test, describe, expect } from 'test/methods.js'; import { http, HttpResponse } from 'msw'; -import { settingsToWasmJson } from './settings.js'; +import { settingsToWasmJson, MAX_RESPONSE_SIZE, MAX_URLS_PER_TRUST_SETTING } from './settings.js'; describe('settings', () => { describe('settingsToWasmJson', () => { @@ -165,6 +165,84 @@ describe('settings', () => { 'Failed to resolve trust settings.' ); }); + + test('should not fetch URLs for unknown keys not defined in TrustSettings', async ({ + requestMock + }) => { + let unknownKeyFetched = false; + requestMock.use( + http.get('http://unknownKey', () => { + unknownKeyFetched = true; + return HttpResponse.text('should not be fetched'); + }), + http.get('http://trustAnchors', () => + HttpResponse.text( + '-----BEGIN CERTIFICATE-----bar-----END CERTIFICATE-----' + ) + ) + ); + + const settingsString = await settingsToWasmJson({ + trust: { + trustAnchors: 'http://trustAnchors', + ...(({ unknownKey: 'http://unknownKey' }) as any) + } + }); + + expect(unknownKeyFetched).toBe(false); + expect(settingsString).toContain('trust_anchors'); + }); + + test('should not crash when a CawgTrustSettings boolean field is present', async () => { + const settingsStringPromise = settingsToWasmJson({ + cawgTrust: { + verifyTrustList: true + } + }); + + await expect(settingsStringPromise).resolves.not.toThrow(); + }); + + test('should only fetch the first MAX_URLS_PER_TRUST_SETTING URLs in an array', async ({ + requestMock + }) => { + const CERT = '-----BEGIN CERTIFICATE-----x-----END CERTIFICATE-----'; + const fetchedUrls: string[] = []; + + requestMock.use( + http.get(/http:\/\/anchor-\d+/, ({ request }) => { + fetchedUrls.push(request.url); + return HttpResponse.text(CERT); + }) + ); + + const urls = Array.from({ length: MAX_URLS_PER_TRUST_SETTING + 1 }, (_, i) => `http://anchor-${i}`); + + await settingsToWasmJson({ + trust: { userAnchors: urls } + }); + + expect(fetchedUrls.length).toBe(MAX_URLS_PER_TRUST_SETTING); + }); + + test('should throw when a fetched response exceeds the size limit', async ({ + requestMock + }) => { + const oversizedBody = 'x'.repeat(MAX_RESPONSE_SIZE + 1); + requestMock.use( + http.get('http://oversized', () => HttpResponse.text(oversizedBody)) + ); + + const settingsStringPromise = settingsToWasmJson({ + trust: { + trustConfig: 'http://oversized' + } + }); + + await expect(settingsStringPromise).rejects.toThrow( + 'Failed to resolve trust settings.' + ); + }); }); }); }); diff --git a/packages/c2pa-web/src/lib/settings.ts b/packages/c2pa-web/src/lib/settings.ts index 360984a..2f78d28 100644 --- a/packages/c2pa-web/src/lib/settings.ts +++ b/packages/c2pa-web/src/lib/settings.ts @@ -77,6 +77,14 @@ export interface TrustSettings { allowedList?: string | string[]; } +const TRUST_SETTINGS_KEY_MAP: Record = { + userAnchors: true, + trustAnchors: true, + trustConfig: true, + allowedList: true +}; +const TRUST_SETTINGS_KEYS = Object.keys(TRUST_SETTINGS_KEY_MAP) as (keyof TrustSettings)[]; + export interface CawgTrustSettings extends TrustSettings { /** * Enable CAWG trust validation. The default value is "true." @@ -112,6 +120,9 @@ const DEFAULT_SETTINGS: Settings = { } }; +export const MAX_RESPONSE_SIZE = 1 * 1024 * 1024; // 1MB +export const MAX_URLS_PER_TRUST_SETTING = 25; + /** * Resolves any trust list URLs and serializes the resulting object into a JSON string of the structure expected by c2pa-rs. * Will merge any provided values on top of the default settings. @@ -161,9 +172,16 @@ function snakeCase(str: string): string { */ async function resolveTrustSettings(settings: TrustSettings): Promise { try { - const promises = Object.entries(settings).map(async ([key, val]) => { - if (Array.isArray(val)) { - const promises = val.map(async (val) => { + const promises = Object.entries(settings) + .filter(([key]) => TRUST_SETTINGS_KEYS.includes(key as keyof TrustSettings)) + .map(async ([key, val]) => { + if (val && typeof val === 'object' && Array.isArray(val)) { + // Only fetch the first MAX_URLS_PER_TRUST_SETTING URLs to prevent excessive resource consumption. + const promises = val.slice(0, MAX_URLS_PER_TRUST_SETTING).map(async (val) => { + if (typeof val !== 'string') { + throw new Error('Expected a string value for array item'); + } + const text = await fetchResource(val); if (shouldValidateKey(key) && !containsCerts(text)) { @@ -176,7 +194,7 @@ async function resolveTrustSettings(settings: TrustSettings): Promise { const result = await Promise.all(promises); const combined = result.join(''); settings[key as keyof TrustSettings] = combined; - } else if (val && isUrl(val)) { + } else if (val && typeof val === 'string' && isUrl(val)) { const text = await fetchResource(val); if (shouldValidateKey(key) && !containsCerts(text)) { @@ -207,5 +225,9 @@ async function fetchResource(url: string): Promise { const res = await fetch(url); const text = await res.text(); + if (text.length > MAX_RESPONSE_SIZE) { + throw new Error(`Response from ${url} is too large. Max size is ${MAX_RESPONSE_SIZE} bytes.`); + } + return text; } From d5cc10a9df06707e9f34ff4a97cfb4f80526ab38 Mon Sep 17 00:00:00 2001 From: ale-adobe Date: Thu, 14 May 2026 15:23:11 -0700 Subject: [PATCH 2/3] Add changeset --- .changeset/forty-dodos-change.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/forty-dodos-change.md diff --git a/.changeset/forty-dodos-change.md b/.changeset/forty-dodos-change.md new file mode 100644 index 0000000..ee7ab54 --- /dev/null +++ b/.changeset/forty-dodos-change.md @@ -0,0 +1,5 @@ +--- +'@contentauth/c2pa-web': patch +--- + +Add checks for valid trust keys, response size, and URL fetch limits From 330517ad3f29a45cae2f3815b76d583488dc95bc Mon Sep 17 00:00:00 2001 From: ale-adobe Date: Thu, 14 May 2026 16:25:11 -0700 Subject: [PATCH 3/3] Remove limit on number of URLs to fetch per key --- packages/c2pa-web/src/lib/settings.spec.ts | 22 ---------------------- packages/c2pa-web/src/lib/settings.ts | 4 +--- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/packages/c2pa-web/src/lib/settings.spec.ts b/packages/c2pa-web/src/lib/settings.spec.ts index 0e5948e..cc60942 100644 --- a/packages/c2pa-web/src/lib/settings.spec.ts +++ b/packages/c2pa-web/src/lib/settings.spec.ts @@ -227,28 +227,6 @@ describe('settings', () => { await expect(settingsStringPromise).resolves.not.toThrow(); }); - test('should only fetch the first MAX_URLS_PER_TRUST_SETTING URLs in an array', async ({ - requestMock - }) => { - const CERT = '-----BEGIN CERTIFICATE-----x-----END CERTIFICATE-----'; - const fetchedUrls: string[] = []; - - requestMock.use( - http.get(/http:\/\/anchor-\d+/, ({ request }) => { - fetchedUrls.push(request.url); - return HttpResponse.text(CERT); - }) - ); - - const urls = Array.from({ length: MAX_URLS_PER_TRUST_SETTING + 1 }, (_, i) => `http://anchor-${i}`); - - await settingsToWasmJson({ - trust: { userAnchors: urls } - }); - - expect(fetchedUrls.length).toBe(MAX_URLS_PER_TRUST_SETTING); - }); - test('should throw when a fetched response exceeds the size limit', async ({ requestMock }) => { diff --git a/packages/c2pa-web/src/lib/settings.ts b/packages/c2pa-web/src/lib/settings.ts index 6431183..b63609c 100644 --- a/packages/c2pa-web/src/lib/settings.ts +++ b/packages/c2pa-web/src/lib/settings.ts @@ -121,7 +121,6 @@ const DEFAULT_SETTINGS: Settings = { }; export const MAX_RESPONSE_SIZE = 1 * 1024 * 1024; // 1MB -export const MAX_URLS_PER_TRUST_SETTING = 25; /** * Resolves any trust list URLs and serializes the resulting object into a JSON string of the structure expected by c2pa-rs. @@ -176,8 +175,7 @@ async function resolveTrustSettings(settings: TrustSettings): Promise { .filter(([key]) => TRUST_SETTINGS_KEYS.includes(key as keyof TrustSettings)) .map(async ([key, val]) => { if (val && typeof val === 'object' && Array.isArray(val)) { - // Only fetch the first MAX_URLS_PER_TRUST_SETTING URLs to prevent excessive resource consumption. - const promises = val.slice(0, MAX_URLS_PER_TRUST_SETTING).map(async (val) => { + const promises = val.map(async (val) => { if (typeof val !== 'string') { throw new Error('Expected a string value for array item'); }