feat(timestamp): add TSA URL validation and sanitization methods

This commit is contained in:
alam00000 committed 2026-07-01 23:23:37 +05:30
1 parent b09fc21e66
commit b21f602cca
7 files changed
+168 -2

No files matched your search

+51
View File
@@ -44,6 +44,20 @@ vi.mock('@/js/workflow/nodes/base-node', () => ({
addOutput() {}
addControl() {}
controls: Record<string, unknown> = {};
sanitizeControlValue(key: string, value: unknown): unknown {
const control = this.controls[key];
if (!control || typeof control !== 'object' || !('value' in control)) {
return value;
}
const current = (control as { value: unknown }).value;
if (current === null || current === undefined) {
return value;
}
if (typeof value !== typeof current) {
return current;
}
return value;
}
},
}));
@@ -174,3 +188,40 @@ describe('TimestampNode', () => {
expect(caught?.cause).toBe(originalError);
});
});
describe('TimestampNode.sanitizeControlValue (workflow import hardening)', () => {
it('keeps a preset tsaUrl from an imported workflow', () => {
const node = new TimestampNode();
expect(
node.sanitizeControlValue('tsaUrl', TIMESTAMP_TSA_PRESETS[0].url)
).toBe(TIMESTAMP_TSA_PRESETS[0].url);
expect(node.sanitizeControlValue('tsaUrl', 'https://freetsa.org/tsr')).toBe(
'https://freetsa.org/tsr'
);
});
it('resets an untrusted imported tsaUrl to the default preset', () => {
const node = new TimestampNode();
expect(
node.sanitizeControlValue(
'tsaUrl',
'https://attacker.example.com/steal-tsr'
)
).toBe(TIMESTAMP_TSA_PRESETS[0].url);
});
it('resets non-string tsaUrl values injected via import', () => {
const node = new TimestampNode();
expect(node.sanitizeControlValue('tsaUrl', { evil: true })).toBe(
TIMESTAMP_TSA_PRESETS[0].url
);
expect(node.sanitizeControlValue('tsaUrl', null)).toBe(
TIMESTAMP_TSA_PRESETS[0].url
);
});
it('does not constrain non-tsaUrl controls', () => {
const node = new TimestampNode();
expect(node.sanitizeControlValue('other', 'anything')).toBe('anything');
});
});
+56
View File
@@ -1,6 +1,8 @@
import { describe, it, expect } from 'vitest';
import {
TIMESTAMP_TSA_PRESETS,
isAllowedTsaUrl,
isValidTsaRequestUrl,
type TimestampTsaPreset,
} from '@/js/config/timestamp-tsa';
@@ -47,3 +49,57 @@ describe('Timestamp TSA Presets', () => {
expect(preset).toHaveProperty('url');
});
});
describe('isAllowedTsaUrl', () => {
it('accepts every built-in preset URL', () => {
for (const preset of TIMESTAMP_TSA_PRESETS) {
expect(isAllowedTsaUrl(preset.url)).toBe(true);
}
});
it('rejects arbitrary attacker-controlled hosts', () => {
expect(isAllowedTsaUrl('https://attacker.example.com/steal-tsr')).toBe(
false
);
expect(isAllowedTsaUrl('http://attacker.example.com')).toBe(false);
});
it('rejects look-alike hosts that merely contain a preset host', () => {
expect(isAllowedTsaUrl('https://timestamp.digicert.com.attacker.com')).toBe(
false
);
expect(isAllowedTsaUrl('https://freetsa.org.attacker.com/tsr')).toBe(false);
});
it('rejects userinfo tricks that resolve to another host', () => {
expect(isAllowedTsaUrl('https://freetsa.org@attacker.com')).toBe(false);
});
it('rejects non-http(s) schemes and non-string values', () => {
expect(isAllowedTsaUrl('javascript:alert(1)')).toBe(false);
expect(isAllowedTsaUrl('file:///etc/passwd')).toBe(false);
expect(isAllowedTsaUrl('data:text/plain,hi')).toBe(false);
expect(isAllowedTsaUrl('not a url')).toBe(false);
expect(isAllowedTsaUrl(null)).toBe(false);
expect(isAllowedTsaUrl(undefined)).toBe(false);
expect(isAllowedTsaUrl(1234)).toBe(false);
expect(isAllowedTsaUrl({ url: 'https://freetsa.org/tsr' })).toBe(false);
});
});
describe('isValidTsaRequestUrl', () => {
it('accepts well-formed http and https URLs', () => {
expect(isValidTsaRequestUrl('http://timestamp.digicert.com')).toBe(true);
expect(isValidTsaRequestUrl('https://tsa.example.org/tsr')).toBe(true);
});
it('rejects dangerous schemes, malformed input, and non-strings', () => {
expect(isValidTsaRequestUrl('javascript:alert(1)')).toBe(false);
expect(isValidTsaRequestUrl('data:text/plain,hi')).toBe(false);
expect(isValidTsaRequestUrl('file:///etc/passwd')).toBe(false);
expect(isValidTsaRequestUrl('')).toBe(false);
expect(isValidTsaRequestUrl(' ')).toBe(false);
expect(isValidTsaRequestUrl(42)).toBe(false);
expect(isValidTsaRequestUrl(null)).toBe(false);
});
});