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 diff --git a/packages/c2pa-web/src/lib/settings.spec.ts b/packages/c2pa-web/src/lib/settings.spec.ts index b99293e..cc60942 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', () => { @@ -189,6 +189,62 @@ 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 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 a018ad2..b63609c 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,8 @@ const DEFAULT_SETTINGS: Settings = { } }; +export const MAX_RESPONSE_SIZE = 1 * 1024 * 1024; // 1MB + /** * 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 +171,15 @@ 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 = 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)) { const promises = val.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 +192,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 +223,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; }