diff --git a/.htaccess b/.htaccess
index 1a21bcc3..81710c99 100644
--- a/.htaccess
+++ b/.htaccess
@@ -12,8 +12,14 @@ Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Embedder-Policy "require-corp"
+Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-eval' blob: https://cdn.jsdelivr.net; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob: https:; font-src 'self' data: https://fonts.gstatic.com https://rawcdn.githack.com; connect-src 'self' blob: https://api.github.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://bentopdf-cors-proxy.bentopdf.workers.dev https://rawcdn.githack.com; object-src 'none'; base-uri 'self'; frame-src 'self' blob:; frame-ancestors 'self'; form-action 'self'"
+# Block source maps from being served
+
+ Require all denied
+
+
# ============================================
# 2. BROWSER CACHING
# ============================================
diff --git a/cloudflare/cors-proxy-worker.js b/cloudflare/cors-proxy-worker.js
index fd5de902..79d7f107 100644
--- a/cloudflare/cors-proxy-worker.js
+++ b/cloudflare/cors-proxy-worker.js
@@ -13,13 +13,11 @@
*/
const ALLOWED_PATH_PATTERNS = [
- /\.crt$/i,
- /\.cer$/i,
- /\.pem$/i,
- /\/certs\//i,
- /\/ocsp/i,
- /\/crl/i,
- /caIssuers/i,
+ /\.(crt|cer|pem|der|p7c|p7b)$/i,
+ /(^|\/)certs(\/|$)/i,
+ /(^|\/)ocsp(\/|$)/i,
+ /(^|\/)crl(\/|$)/i,
+ /(^|\/)caissuers(\/|$)/i,
];
const ALLOWED_TSA_HOSTS = new Set([
@@ -123,6 +121,31 @@ function isPrivateOrReservedHost(hostname) {
return false;
}
+async function hostnameResolvesToPrivate(hostname) {
+ const clean = hostname.replace(/^\[|\]$/g, '');
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(clean) || clean.includes(':')) {
+ return isPrivateOrReservedHost(clean);
+ }
+ const ips = [];
+ for (const type of ['A', 'AAAA']) {
+ try {
+ const res = await fetch(
+ `https://1.1.1.1/dns-query?name=${encodeURIComponent(clean)}&type=${type}`,
+ { headers: { accept: 'application/dns-json' } }
+ );
+ if (!res.ok) return true;
+ const data = await res.json();
+ for (const ans of data.Answer || []) {
+ if (ans && ans.data) ips.push(String(ans.data).replace(/\.$/, ''));
+ }
+ } catch {
+ return true;
+ }
+ }
+ if (ips.length === 0) return true;
+ return ips.some((ip) => isPrivateOrReservedHost(ip));
+}
+
function isValidCertificateUrl(urlString) {
try {
const url = new URL(urlString);
@@ -181,7 +204,7 @@ function handleOptions(request) {
}
export default {
- async fetch(request, env, ctx) {
+ async fetch(request, env) {
const url = new URL(request.url);
const origin = request.headers.get('Origin');
@@ -293,7 +316,8 @@ export default {
const now = Date.now();
if (
isNaN(requestTime) ||
- Math.abs(now - requestTime) > MAX_TIMESTAMP_AGE_MS
+ requestTime > now + 60000 ||
+ now - requestTime > MAX_TIMESTAMP_AGE_MS
) {
return new Response(
JSON.stringify({
@@ -384,9 +408,33 @@ export default {
);
}
+ let targetHostname;
+ try {
+ targetHostname = new URL(targetUrl).hostname;
+ } catch {
+ targetHostname = '';
+ }
+ if (!targetHostname || (await hostnameResolvesToPrivate(targetHostname))) {
+ return new Response(
+ JSON.stringify({
+ error: 'Forbidden destination',
+ message: 'The requested host is not permitted',
+ }),
+ {
+ status: 403,
+ headers: {
+ ...corsHeaders(origin),
+ 'Content-Type': 'application/json',
+ },
+ }
+ );
+ }
+
try {
const upstreamInit = {
method: request.method,
+ redirect: 'manual',
+ signal: AbortSignal.timeout(10000),
headers: {
'User-Agent': 'BentoPDF-CertProxy/1.0',
},
@@ -399,6 +447,22 @@ export default {
const response = await fetch(targetUrl, upstreamInit);
+ if (response.status >= 300 && response.status < 400) {
+ return new Response(
+ JSON.stringify({
+ error: 'Redirect blocked',
+ message: 'Upstream redirects are not allowed',
+ }),
+ {
+ status: 502,
+ headers: {
+ ...corsHeaders(origin),
+ 'Content-Type': 'application/json',
+ },
+ }
+ );
+ }
+
if (!response.ok) {
return new Response(
JSON.stringify({
@@ -480,7 +544,7 @@ export default {
response.headers.get('Content-Type')
),
'Content-Length': totalSize.toString(),
- 'Cache-Control': 'public, max-age=86400',
+ 'Cache-Control': 'no-store',
'X-Content-Type-Options': 'nosniff',
},
});
diff --git a/package.json b/package.json
index deee2635..be955fcb 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,8 @@
"lint:security": "eslint . --no-inline-config --rule 'no-unsanitized/method:error' --rule 'no-unsanitized/property:error' --rule 'security/detect-eval-with-expression:error'",
"security:codeql": "codeql database create ./codeql-db --language=javascript-typescript --source-root=. --overwrite --threads=0 && codeql database analyze ./codeql-db --format=sarif-latest --output=codeql-results.sarif --threads=0 codeql/javascript-queries:codeql-suites/javascript-security-extended.qls",
"security:codeql:quick": "codeql database analyze ./codeql-db --format=csv --output=codeql-results.csv --threads=0 codeql/javascript-queries:codeql-suites/javascript-security-extended.qls",
- "security:audit": "npm audit --audit-level=high && npm run lint:security",
+ "security:patterns": "node scripts/security-patterns-check.mjs",
+ "security:audit": "npm audit --audit-level=high && npm run lint:security && npm run security:patterns",
"prepare": "husky"
},
"devDependencies": {
diff --git a/public/pdfjs-viewer/form-viewer.html b/public/pdfjs-viewer/form-viewer.html
index 42a79b94..cfdc7772 100644
--- a/public/pdfjs-viewer/form-viewer.html
+++ b/public/pdfjs-viewer/form-viewer.html
@@ -220,6 +220,7 @@
// Listen for messages from parent window
window.addEventListener('message', async (event) => {
+ if (event.origin !== window.location.origin) return;
if (event.data.type === 'loadPDF') {
const { data } = event.data;
loadPDF(data);
diff --git a/public/pdfjs-viewer/sign-viewer.html b/public/pdfjs-viewer/sign-viewer.html
index 8e6d05ea..44397cf8 100644
--- a/public/pdfjs-viewer/sign-viewer.html
+++ b/public/pdfjs-viewer/sign-viewer.html
@@ -239,6 +239,7 @@
// Listen for messages from parent window
window.addEventListener('message', async (event) => {
+ if (event.origin !== window.location.origin) return;
console.log('Sign viewer received message:', event.data.type);
if (event.data.type === 'loadPDF') {
diff --git a/scripts/security-patterns-check.mjs b/scripts/security-patterns-check.mjs
new file mode 100644
index 00000000..7f4fcd72
--- /dev/null
+++ b/scripts/security-patterns-check.mjs
@@ -0,0 +1,121 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.resolve(__dirname, '..');
+const SCAN_DIRS = ['src/js', 'bentopdf-pymupdf-wasm/src', 'cloudflare'];
+const SKIP = ['node_modules', 'dist', '.git', 'coverage', 'tests', 'test'];
+
+const findings = [];
+
+function walk(dir) {
+ let entries;
+ try {
+ entries = fs.readdirSync(dir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+ for (const e of entries) {
+ const full = path.join(dir, e.name);
+ if (
+ SKIP.some(
+ (s) =>
+ full.includes(`${path.sep}${s}${path.sep}`) ||
+ full.endsWith(`${path.sep}${s}`)
+ )
+ ) {
+ continue;
+ }
+ if (e.isDirectory()) walk(full);
+ else if (/\.(ts|js|mjs)$/.test(e.name) && !/\.test\.|\.d\.ts$/.test(e.name))
+ checkFile(full);
+ }
+}
+
+function checkFile(file) {
+ const rel = path.relative(ROOT, file);
+ const src = fs.readFileSync(file, 'utf-8');
+ const lines = src.split('\n');
+
+ // Rule 1: no raw string interpolation into a Python/eval template.
+ const codeSink = /(runPython|\.eval)\s*\(\s*`/;
+ for (let i = 0; i < lines.length; i++) {
+ if (codeSink.test(lines[i])) {
+ for (let j = i; j < Math.min(i + 60, lines.length); j++) {
+ const interps = lines[j].match(/\$\{[^}]+\}/g) || [];
+ for (const it of interps) {
+ const danger =
+ /^\$\{\s*(text|name|uri|url|format|password|passwd|title|label|filename|content|search|searchText|query|font|fontname|intent|usage|replaceText)\s*(\}|\.|\))/.test(
+ it
+ );
+ const safe = /pyStr|JSON\.stringify|escapeHtml/.test(it);
+ if (danger && !safe) {
+ findings.push({
+ rule: 'code-injection',
+ file: rel,
+ line: j + 1,
+ detail: `Unescaped user-string interpolation ${it} into a Python/eval template — pass it via pyStr()/JSON.stringify()/globals.set().`,
+ });
+ }
+ }
+ if (lines[j].includes('`)') || lines[j].trim().endsWith('`);')) break;
+ }
+ }
+ }
+
+ // Rule 2: string mutation applied AFTER a sanitize call (sanitize-then-mutate).
+ const sanitizeLine = lines.findIndex((l) =>
+ /DOMPurify\.sanitize|sanitizeEmailHtml|sanitize\(/.test(l)
+ );
+ if (sanitizeLine !== -1) {
+ for (
+ let j = sanitizeLine + 1;
+ j < Math.min(sanitizeLine + 40, lines.length);
+ j++
+ ) {
+ if (
+ /\.replace\(|innerHTML\s*=|\+=\s*`/.test(lines[j]) &&
+ !/escapeHtml|escapeHTML|DOMPurify/.test(lines[j])
+ ) {
+ findings.push({
+ rule: 'sanitize-then-mutate',
+ file: rel,
+ line: j + 1,
+ detail: `Possible mutation of sanitized HTML. Escape/validate at the final sink, or re-sanitize after mutation.`,
+ });
+ break;
+ }
+ }
+ }
+}
+
+for (const d of SCAN_DIRS) walk(path.join(ROOT, d));
+
+const blocking = findings.filter((f) => f.rule === 'code-injection');
+const warnings = findings.filter((f) => f.rule !== 'code-injection');
+
+for (const f of warnings) {
+ console.warn(` ⚠ [${f.rule}] ${f.file}:${f.line}\n ${f.detail}`);
+}
+if (warnings.length) {
+ console.warn(
+ `\n${warnings.length} sanitize-then-mutate warning(s) — review each: escape/validate at the final sink.\n`
+ );
+}
+
+if (blocking.length === 0) {
+ console.log('✅ security-patterns-check: no code-injection patterns found');
+ process.exit(0);
+}
+
+console.error(
+ `\n❌ security-patterns-check found ${blocking.length} code-injection issue(s):\n`
+);
+for (const f of blocking) {
+ console.error(` [${f.rule}] ${f.file}:${f.line}\n ${f.detail}\n`);
+}
+console.error(
+ 'This pattern caused a prior CVE (pymupdf runPython injection). Pass values via pyStr()/JSON.stringify()/globals.set().\n'
+);
+process.exit(1);
diff --git a/src/js/logic/bookmark-pdf.ts b/src/js/logic/bookmark-pdf.ts
index 2ba4294a..ae938152 100644
--- a/src/js/logic/bookmark-pdf.ts
+++ b/src/js/logic/bookmark-pdf.ts
@@ -273,7 +273,7 @@ placeholder="${escapeHTML(field.placeholder || '')}" />
-
@@ -1703,7 +1703,7 @@ function createNodeElement(node: BookmarkNode, level = 0): HTMLLIElement {
titleDiv.innerHTML = `
${escapeHTML(node.title)}${destinationIcon}
- Page ${node.page}
+ Page ${escapeHTML(String(node.page))}
`;
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);
}
}
diff --git a/src/js/logic/cbz-to-pdf-page.ts b/src/js/logic/cbz-to-pdf-page.ts
index f8d18167..2730b172 100644
--- a/src/js/logic/cbz-to-pdf-page.ts
+++ b/src/js/logic/cbz-to-pdf-page.ts
@@ -134,9 +134,27 @@ async function convertCbzToPdf(file: File): Promise {
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);
diff --git a/src/js/logic/email-to-pdf.ts b/src/js/logic/email-to-pdf.ts
index 481fb339..bc812019 100644
--- a/src/js/logic/email-to-pdf.ts
+++ b/src/js/logic/email-to-pdf.ts
@@ -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;
});
diff --git a/src/js/logic/form-creator.ts b/src/js/logic/form-creator.ts
index 318b482f..387c407d 100644
--- a/src/js/logic/form-creator.ts
+++ b/src/js/logic/form-creator.ts
@@ -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(
'[id^="back-to-tools"]'
-) as NodeListOf;
+);
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'
) {
diff --git a/src/js/logic/validate-signature-pdf-page.ts b/src/js/logic/validate-signature-pdf-page.ts
index 71d343fb..b67fbb53 100644
--- a/src/js/logic/validate-signature-pdf-page.ts
+++ b/src/js/logic/validate-signature-pdf-page.ts
@@ -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) => {
diff --git a/src/js/logic/validate-signature-pdf.ts b/src/js/logic/validate-signature-pdf.ts
index 7ebeb02e..7525aae7 100644
--- a/src/js/logic/validate-signature-pdf.ts
+++ b/src/js/logic/validate-signature-pdf.ts
@@ -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) {
diff --git a/src/js/workflow/nodes/redact-node.ts b/src/js/workflow/nodes/redact-node.ts
index 2160d4b2..d9175705 100644
--- a/src/js/workflow/nodes/redact-node.ts
+++ b/src/js/workflow/nodes/redact-node.ts
@@ -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) {