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

+23
View File
@@ -12,3 +12,26 @@ export const TIMESTAMP_TSA_PRESETS: TimestampTsaPreset[] = [
{ label: 'FreeTSA', url: 'https://freetsa.org/tsr' },
{ label: 'MeSign', url: 'http://tsa.mesign.com' },
];
const ALLOWED_TSA_HOSTS: ReadonlySet<string> = new Set(
TIMESTAMP_TSA_PRESETS.map((preset) => new URL(preset.url).hostname)
);
export function isValidTsaRequestUrl(value: unknown): value is string {
if (typeof value !== 'string') {
return false;
}
try {
const url = new URL(value);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch {
return false;
}
}
export function isAllowedTsaUrl(value: unknown): value is string {
if (!isValidTsaRequestUrl(value)) {
return false;
}
return ALLOWED_TSA_HOSTS.has(new URL(value).hostname);
}
+7
View File
@@ -1,6 +1,7 @@
import { PdfSigner, type SignOption } from 'zgapdfsigner';
import forge from 'node-forge';
import { CertificateData, SignPdfOptions } from '@/types';
import { isValidTsaRequestUrl } from '../config/timestamp-tsa.js';
export function parsePfxFile(
pfxBytes: ArrayBuffer,
@@ -349,6 +350,12 @@ export async function timestampPdf(
pdfBytes: Uint8Array,
tsaUrl: string
): Promise<Uint8Array> {
if (!isValidTsaRequestUrl(tsaUrl)) {
throw new Error(
`Invalid TSA URL. The timestamp authority must be a valid http:// or https:// URL.`
);
}
const pageIsHttps =
typeof window !== 'undefined' && window.location?.protocol === 'https:';
const tsaIsHttp = /^http:\/\//i.test(tsaUrl);
+15
View File
@@ -28,4 +28,19 @@ export abstract class BaseWorkflowNode extends ClassicPreset.Node {
description: this.description,
};
}
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;
}
}
+12 -1
View File
@@ -4,7 +4,10 @@ import { pdfSocket } from '../sockets';
import type { SocketData } from '../types';
import { requirePdfInput, processBatch } from '../types';
import { wfError } from '../errors';
import { TIMESTAMP_TSA_PRESETS } from '../../config/timestamp-tsa.js';
import {
TIMESTAMP_TSA_PRESETS,
isAllowedTsaUrl,
} from '../../config/timestamp-tsa.js';
import { timestampPdf } from '../../logic/digital-sign-pdf.js';
import { loadPdfDocument } from '../../utils/load-pdf-document.js';
@@ -32,6 +35,14 @@ export class TimestampNode extends BaseWorkflowNode {
return TIMESTAMP_TSA_PRESETS;
}
sanitizeControlValue(key: string, value: unknown): unknown {
const sanitized = super.sanitizeControlValue(key, value);
if (key === 'tsaUrl' && !isAllowedTsaUrl(sanitized)) {
return TIMESTAMP_TSA_PRESETS[0].url;
}
return sanitized;
}
async data(
inputs: Record<string, SocketData[]>
): Promise<Record<string, SocketData>> {
+4 -1
View File
@@ -98,7 +98,10 @@ async function deserializeWorkflow(
for (const [key, value] of Object.entries(serializedNode.controls || {})) {
const control = node.controls[key];
if (control && 'value' in control) {
(control as { value: unknown }).value = value;
(control as { value: unknown }).value = node.sanitizeControlValue(
key,
value
);
}
}