Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/forty-dodos-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@contentauth/c2pa-web': patch
---

Add checks for valid trust keys, response size, and URL fetch limits
58 changes: 57 additions & 1 deletion packages/c2pa-web/src/lib/settings.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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.'
);
});
});
});
});
26 changes: 23 additions & 3 deletions packages/c2pa-web/src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ export interface TrustSettings {
allowedList?: string | string[];
}

const TRUST_SETTINGS_KEY_MAP: Record<keyof TrustSettings, true> = {
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."
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -161,9 +171,15 @@ function snakeCase(str: string): string {
*/
async function resolveTrustSettings(settings: TrustSettings): Promise<void> {
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)) {
Expand All @@ -176,7 +192,7 @@ async function resolveTrustSettings(settings: TrustSettings): Promise<void> {
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)) {
Expand Down Expand Up @@ -207,5 +223,9 @@ async function fetchResource(url: string): Promise<string> {
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;
}
Loading