feat(security): enhance security policies and implement input validation across multiple files

This commit is contained in:
alam00000
2026-07-14 19:46:40 +05:30
parent 0ef279e0da
commit 58cd1a8893
13 changed files with 293 additions and 52 deletions
+29 -3
View File
@@ -273,7 +273,7 @@ placeholder="${escapeHTML(field.placeholder || '')}" />
<div class="grid grid-cols-2 gap-2">
<div>
<label class="text-xs text-gray-600">Page</label>
<input type="number" id="modal-dest-page" min="1" max="${field.maxPages || 1}" value="${defaultValues.destPage || field.page || 1}"
<input type="number" id="modal-dest-page" min="1" max="${escapeHTML(String(field.maxPages || 1))}" value="${escapeHTML(String(defaultValues.destPage || field.page || 1))}"
class="w-full px-2 py-1 border border-gray-300 rounded text-sm text-gray-900" step="1" />
</div>
<div>
@@ -1703,7 +1703,7 @@ function createNodeElement(node: BookmarkNode, level = 0): HTMLLIElement {
titleDiv.innerHTML = `
<span class="text-sm block ${styleClass} ${textColorClass}" ${customColorStyle}>${escapeHTML(node.title)}${destinationIcon}</span>
<span class="text-xs text-gray-500">Page ${node.page}</span>
<span class="text-xs text-gray-500">Page ${escapeHTML(String(node.page))}</span>
`;
titleDiv.addEventListener('click', async () => {
@@ -1951,7 +1951,14 @@ exportCsvBtn?.addEventListener('click', () => {
const csv =
'title,page,level\n' +
flat
.map((b) => `"${b.title.replace(/"/g, '""')}",${b.page},${b.level}`)
.map((b) => {
const first = b.title.charAt(0);
const t =
/[=+\-@]/.test(first) || first === '\t' || first === '\r'
? `'${b.title}`
: b.title;
return `"${t.replace(/"/g, '""')}",${b.page},${b.level}`;
})
.join('\n');
const blob = new Blob([csv], { type: 'text/csv' });
@@ -2007,8 +2014,12 @@ jsonImportHidden?.addEventListener('change', async (e: Event) => {
const imported = JSON.parse(text) as BookmarkTree;
function cleanImportedTree(nodes: BookmarkNode[]): void {
if (!nodes) return;
const validStyles = ['bold', 'italic', 'bold-italic'];
for (const node of nodes) {
if (node.title) node.title = cleanTitle(node.title);
node.page = Number.isFinite(Number(node.page))
? Math.max(1, Math.trunc(Number(node.page)))
: 1;
if (
typeof node.color === 'string' &&
node.color.startsWith('#') &&
@@ -2016,6 +2027,21 @@ jsonImportHidden?.addEventListener('change', async (e: Event) => {
) {
node.color = '';
}
if (!validStyles.includes(node.style as string)) {
node.style = null;
}
node.destX =
node.destX != null && Number.isFinite(Number(node.destX))
? Number(node.destX)
: null;
node.destY =
node.destY != null && Number.isFinite(Number(node.destY))
? Number(node.destY)
: null;
node.zoom =
node.zoom != null && /^-?\d*\.?\d+$/.test(String(node.zoom))
? String(node.zoom)
: null;
if (node.children) cleanImportedTree(node.children);
}
}
+18
View File
@@ -134,9 +134,27 @@ async function convertCbzToPdf(file: File): Promise<Blob> {
a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })
);
const MAX_CBZ_PAGES = 2000;
const MAX_ENTRY_BYTES = 100 * 1024 * 1024;
const MAX_TOTAL_BYTES = 500 * 1024 * 1024;
if (imageFiles.length > MAX_CBZ_PAGES) {
throw new Error(`Archive has too many images (max ${MAX_CBZ_PAGES}).`);
}
let totalBytes = 0;
for (const filename of imageFiles) {
const zipEntry = zip.files[filename];
const declared = (
zipEntry as unknown as { _data?: { uncompressedSize?: number } }
)._data?.uncompressedSize;
if (typeof declared === 'number' && declared > MAX_ENTRY_BYTES) {
throw new Error('Archive contains an oversized image entry.');
}
const imageData = await zipEntry.async('arraybuffer');
totalBytes += imageData.byteLength;
if (totalBytes > MAX_TOTAL_BYTES) {
throw new Error('Archive is too large when decompressed.');
}
const dataArray = new Uint8Array(imageData);
const actualFormat = detectImageFormat(dataArray);
+7 -1
View File
@@ -217,7 +217,13 @@ function processInlineImages(
const att = cidMap.get(cid);
if (att && att.content) {
const base64 = uint8ArrayToBase64(att.content);
return `src="data:${att.contentType};base64,${base64}"`;
const safeType =
/^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]*$/.test(
att.contentType
)
? att.contentType
: 'application/octet-stream';
return `src="data:${safeType};base64,${base64}"`;
}
return match;
});
+7 -19
View File
@@ -90,9 +90,6 @@ const pdfFileInput = document.getElementById(
'pdfFileInput'
) as HTMLInputElement;
const blankPdfBtn = document.getElementById('blankPdfBtn') as HTMLButtonElement;
const pdfUploadInput = document.getElementById(
'pdfUploadInput'
) as HTMLInputElement;
const pageSizeSelector = document.getElementById(
'pageSizeSelector'
) as HTMLDivElement;
@@ -117,9 +114,6 @@ const nextPageBtn = document.getElementById('nextPageBtn') as HTMLButtonElement;
const addPageBtn = document.getElementById('addPageBtn') as HTMLButtonElement;
const resetBtn = document.getElementById('resetBtn') as HTMLButtonElement;
const downloadBtn = document.getElementById('downloadBtn') as HTMLButtonElement;
const backToToolsBtn = document.getElementById(
'back-to-tools'
) as HTMLButtonElement | null;
const gotoPageInput = document.getElementById(
'gotoPageInput'
) as HTMLInputElement;
@@ -794,14 +788,12 @@ function renderField(field: FormField): void {
});
// Touch events for moving fields
let touchMoveStarted = false;
fieldWrapper.addEventListener(
'touchstart',
(e) => {
if ((e.target as HTMLElement).classList.contains('resize-handle')) {
return;
}
touchMoveStarted = false;
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
offsetX = touch.clientX - rect.left - field.x;
@@ -813,7 +805,6 @@ function renderField(field: FormField): void {
fieldWrapper.addEventListener('touchmove', (e) => {
e.preventDefault();
touchMoveStarted = true;
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
let newX = touch.clientX - rect.left - offsetX;
@@ -829,10 +820,6 @@ function renderField(field: FormField): void {
field.y = newY;
});
fieldWrapper.addEventListener('touchend', () => {
touchMoveStarted = false;
});
// Add resize handles to the container - hidden by default
const handles = ['nw', 'ne', 'sw', 'se', 'n', 's', 'e', 'w'];
handles.forEach((pos) => {
@@ -2381,7 +2368,6 @@ downloadBtn.addEventListener('click', async () => {
else if (field.options && field.options.length > 0)
dropdown.select(field.options[0]);
const rgbColor = hexToRgb(field.textColor);
dropdown.acroField.setFontSize(field.fontSize);
dropdown.acroField.setDefaultAppearance(
`0 0 0 rg /Helv ${field.fontSize} Tf`
@@ -2417,7 +2403,6 @@ downloadBtn.addEventListener('click', async () => {
else if (field.options && field.options.length > 0)
optionList.select(field.options[0]);
const rgbColor = hexToRgb(field.textColor);
optionList.acroField.setFontSize(field.fontSize);
optionList.acroField.setDefaultAppearance(
`0 0 0 rg /Helv ${field.fontSize} Tf`
@@ -2547,7 +2532,10 @@ downloadBtn.addEventListener('click', async () => {
});
// Add Date Format and Keystroke Actions to the FIELD (not widget)
const dateFormat = field.dateFormat || 'mm/dd/yyyy';
const dateFormat = (field.dateFormat || 'mm/dd/yyyy').replace(
/[^a-zA-Z0-9/:.,\- ]/g,
''
);
const formatAction = pdfDoc.context.obj({
Type: 'Action',
@@ -2757,9 +2745,9 @@ downloadBtn.addEventListener('click', async () => {
});
// Back to tools button
const backToToolsBtns = document.querySelectorAll(
const backToToolsBtns = document.querySelectorAll<HTMLButtonElement>(
'[id^="back-to-tools"]'
) as NodeListOf<HTMLButtonElement>;
);
backToToolsBtns.forEach((btn) => {
btn.addEventListener('click', () => {
window.location.href = import.meta.env.BASE_URL;
@@ -3129,7 +3117,7 @@ let modalCloseCallback: (() => void) | null = null;
function showModal(
title: string,
message: string,
type: 'error' | 'warning' | 'info' = 'error',
_type: 'error' | 'warning' | 'info' = 'error',
onClose?: () => void,
buttonText: string = 'Close'
) {
+5 -3
View File
@@ -322,7 +322,7 @@ function displayResults(): void {
'mb-4 p-3 bg-gray-700 rounded-lg border border-gray-600';
const validCount = state.results.filter(
(r) => r.isValid && !r.isExpired
(r) => r.isValid && !r.isExpired && r.isTrusted
).length;
const trustVerified = state.trustedCert
? state.results.filter((r) => r.isTrusted).length
@@ -396,10 +396,12 @@ function createSignatureCard(
statusColor = 'text-yellow-400';
statusIcon = 'alert-triangle';
statusText = 'Certificate Expired';
} else if (result.isSelfSigned) {
} else if (!result.isTrusted) {
statusColor = 'text-yellow-400';
statusIcon = 'alert-triangle';
statusText = 'Self-Signed Certificate';
statusText = result.isSelfSigned
? 'Self-Signed — Signer Identity Not Verified'
: 'Signature Intact — Signer Identity Not Verified';
}
const formatDate = (date: Date) => {
+19 -14
View File
@@ -145,25 +145,30 @@ export async function validateSignature(
result.isSelfSigned = signerCert.isIssuer(signerCert);
// Check trust against provided certificate
// Check trust against provided certificate (cryptographic, not name/serial match)
if (trustedCert) {
try {
const isTrustedIssuer = trustedCert.isIssuer(signerCert);
const isSameCert = signerCert.serialNumber === trustedCert.serialNumber;
const trustedPem = forge.pki.certificateToPem(trustedCert);
const isSameCert =
forge.pki.certificateToPem(signerCert) === trustedPem;
let chainTrusted = false;
for (const cert of p7.certificates) {
if (
trustedCert.isIssuer(cert) ||
(cert as forge.pki.Certificate).serialNumber ===
trustedCert.serialNumber
) {
chainTrusted = true;
break;
let cryptoIssued = false;
const chain = [
signerCert,
...(p7.certificates as forge.pki.Certificate[]),
];
for (const cert of chain) {
try {
if (trustedCert.verify(cert)) {
cryptoIssued = true;
break;
}
} catch {
continue;
}
}
result.isTrusted = isTrustedIssuer || isSameCert || chainTrusted;
result.isTrusted = isSameCert || cryptoIssued;
} catch {
result.isTrusted = false;
}
@@ -223,7 +228,7 @@ export async function validateSignature(
}
if (signature.byteRange && signature.byteRange.length === 4) {
const [, len1, start2, len2] = signature.byteRange;
const [, , start2, len2] = signature.byteRange;
const expectedEnd = start2 + len2;
if (expectedEnd === pdfBytes.length) {
+3 -1
View File
@@ -67,7 +67,9 @@ export class RedactNode extends BaseWorkflowNode {
const pdfInputs = requirePdfInput(inputs, 'Redact');
const mode = this.getText('redactMode', 'text');
const searchText = this.getText('text', '');
const searchText = this.getText('text', '')
.replace(/\\/g, '')
.replace(/\p{Cc}/gu, '');
const fill = hexToRgb(this.getText('fillColor', '#000000'));
if (mode === 'text' && !searchText) {