mirror of
https://github.com/alam00000/bentopdf.git
synced 2026-08-25 16:16:49 +00:00
feat(security): enhance security policies and implement input validation across multiple files
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user