feat(Security): fixes
Some checks failed
Build and Push Docker Images (Default + Simple Mode) / build-and-release (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-amd64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:default simple_mode:false suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / build-arm64 (map[name:simple simple_mode:true suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / merge-manifests-ghcr (map[name:simple suffix:-simple]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:default suffix:]) (push) Has been cancelled
Build and Push Docker Images (Default + Simple Mode) / push-to-dockerhub (map[name:simple suffix:-simple]) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy static content to Pages / deploy (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile name:bentopdf]) (push) Has been cancelled
Trivy Security Scan / scan-image (map[file:Dockerfile.nonroot name:bentopdf-nonroot]) (push) Has been cancelled
Trivy Security Scan / scan-dependencies (push) Has been cancelled
Trivy Security Scan / scan-config (push) Has been cancelled

This commit is contained in:
alam00000
2026-04-18 16:58:55 +05:30
parent b040aef729
commit 7527187812
8 changed files with 371 additions and 156 deletions

76
package-lock.json generated
View File

@@ -33,6 +33,7 @@
"diff": "^8.0.3", "diff": "^8.0.3",
"dompurify": "^3.4.0", "dompurify": "^3.4.0",
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz", "embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
"fast-xml-parser": "^5.7.1",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",
@@ -2074,6 +2075,18 @@
"integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==", "integrity": "sha512-ObyTmabopTMwN6AGwrEkmDbdmM5wnwlRdn9jnJYQ2KbBUPdPDcfNf1QkcWFOVD+qIgSqILz9nK51e22T+x+gkQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/@nodable/entities": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz",
"integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/nodable"
}
],
"license": "MIT"
},
"node_modules/@oxc-project/types": { "node_modules/@oxc-project/types": {
"version": "0.122.0", "version": "0.122.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
@@ -6993,6 +7006,42 @@
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)" "license": "(MIT AND Zlib)"
}, },
"node_modules/fast-xml-builder": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz",
"integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"path-expression-matcher": "^1.1.3"
}
},
"node_modules/fast-xml-parser": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.1.tgz",
"integrity": "sha512-8Cc3f8GUGUULg34pBch/KGyPLglS+OFs05deyOlY7fL2MTagYPKrVQNmR1fLF/yJ9PH5ZSTd3YDF6pnmeZU+zA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"dependencies": {
"@nodable/entities": "^2.1.0",
"fast-xml-builder": "^1.1.5",
"path-expression-matcher": "^1.5.0",
"strnum": "^2.2.3"
},
"bin": {
"fxparser": "src/cli/cli.js"
}
},
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
@@ -9861,6 +9910,21 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/path-expression-matcher": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz",
"integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/path-key": { "node_modules/path-key": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
@@ -11347,6 +11411,18 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1" "url": "https://github.com/chalk/ansi-regex?sponsor=1"
} }
}, },
"node_modules/strnum": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz",
"integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/stylis": { "node_modules/stylis": {
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",

View File

@@ -94,6 +94,7 @@
"diff": "^8.0.3", "diff": "^8.0.3",
"dompurify": "^3.4.0", "dompurify": "^3.4.0",
"embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz", "embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz",
"fast-xml-parser": "^5.7.1",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"html2canvas": "^1.4.1", "html2canvas": "^1.4.1",

View File

@@ -330,6 +330,22 @@ async function cacheInBatches(cache, urls, batchSize = 5) {
self.addEventListener('message', (event) => { self.addEventListener('message', (event) => {
if (!event.data) return; if (!event.data) return;
if (event.origin && event.origin !== self.location.origin) {
return;
}
const source = event.source;
if (source && typeof source === 'object' && 'url' in source) {
try {
const sourceOrigin = new URL(source.url).origin;
if (sourceOrigin !== self.location.origin) {
return;
}
} catch (e) {
return;
}
}
if (event.data.type === 'SKIP_WAITING') { if (event.data.type === 'SKIP_WAITING') {
self.skipWaiting(); self.skipWaiting();
return; return;

View File

@@ -43,7 +43,26 @@ interface ErrorResult {
message: string; message: string;
} }
self.onmessage = function (e: MessageEvent<WorkerMessage>) { function isValidMessage(data: unknown): data is WorkerMessage {
if (!data || typeof data !== 'object') return false;
const m = data as Record<string, unknown>;
if (typeof m.id !== 'number') return false;
if (m.type === 'diff') {
return Array.isArray(m.beforeItems) && Array.isArray(m.afterItems);
}
if (m.type === 'pair') {
return Array.isArray(m.leftPages) && Array.isArray(m.rightPages);
}
return false;
}
self.onmessage = function (e: MessageEvent<unknown>) {
if (e.origin && e.origin !== '' && e.origin !== self.location.origin) {
return;
}
if (!isValidMessage(e.data)) {
return;
}
const msg = e.data; const msg = e.data;
try { try {
if (msg.type === 'diff') { if (msg.type === 'diff') {

View File

@@ -720,7 +720,14 @@ async function handleMultiFileUpload(toolId: string) {
'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden'; 'w-full h-36 sm:h-40 md:h-44 bg-gray-900 rounded-md border-2 border-gray-600 flex items-center justify-center overflow-hidden';
const img = document.createElement('img'); const img = document.createElement('img');
img.src = url; try {
const parsed = new URL(url);
if (parsed.protocol === 'blob:') {
img.src = parsed.href;
}
} catch {
console.warn('Invalid blob URL for preview');
}
img.className = 'max-w-full max-h-full object-contain'; img.className = 'max-w-full max-h-full object-contain';
const p = document.createElement('p'); const p = document.createElement('p');

View File

@@ -203,9 +203,16 @@ function initializePage(): void {
if (sigImageThumb && sigImagePreview) { if (sigImageThumb && sigImagePreview) {
const url = URL.createObjectURL(file); const url = URL.createObjectURL(file);
sigImageThumb.src = url; try {
const parsed = new URL(url);
if (parsed.protocol === 'blob:') {
sigImageThumb.src = parsed.href;
sigImagePreview.classList.remove('hidden'); sigImagePreview.classList.remove('hidden');
} }
} catch {
console.warn('Invalid blob URL for signature preview');
}
}
} }
}); });
} }

View File

@@ -19,6 +19,7 @@ type LucideWindow = Window & {
}; };
}; };
import DOMPurify from 'dompurify';
import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js'; import { initializeGlobalShortcuts } from '../utils/shortcuts-init.js';
import { downloadFile, escapeHtml, hexToRgb } from '../utils/helpers.js'; import { downloadFile, escapeHtml, hexToRgb } from '../utils/helpers.js';
import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js';
@@ -1327,7 +1328,7 @@ function showProperties(field: FormField): void {
`; `;
} }
propertiesPanel.innerHTML = ` const propertiesHtml = `
<div class="space-y-3"> <div class="space-y-3">
<div> <div>
<label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label> <label class="block text-xs font-semibold text-gray-300 mb-1">Field Name ${field.type === 'radio' ? '(Group Name)' : ''}</label>
@@ -1399,6 +1400,10 @@ function showProperties(field: FormField): void {
</div> </div>
`; `;
propertiesPanel.innerHTML = DOMPurify.sanitize(propertiesHtml, {
ADD_ATTR: ['target'],
});
// Common listeners // Common listeners
const propName = document.getElementById('propName') as HTMLInputElement; const propName = document.getElementById('propName') as HTMLInputElement;
const nameError = document.getElementById('nameError') as HTMLDivElement; const nameError = document.getElementById('nameError') as HTMLDivElement;

View File

@@ -1,5 +1,6 @@
import { jsPDF } from 'jspdf'; import { jsPDF } from 'jspdf';
import autoTable from 'jspdf-autotable'; import autoTable from 'jspdf-autotable';
import { XMLParser } from 'fast-xml-parser';
export interface XmlToPdfOptions { export interface XmlToPdfOptions {
onProgress?: (percent: number, message: string) => void; onProgress?: (percent: number, message: string) => void;
@@ -9,6 +10,9 @@ interface jsPDFWithAutoTable extends jsPDF {
lastAutoTable?: { finalY: number }; lastAutoTable?: { finalY: number };
} }
const ATTR_PREFIX = '@_';
const TEXT_KEY = '#text';
export async function convertXmlToPdf( export async function convertXmlToPdf(
file: File, file: File,
options?: XmlToPdfOptions options?: XmlToPdfOptions
@@ -16,48 +20,82 @@ export async function convertXmlToPdf(
const { onProgress } = options || {}; const { onProgress } = options || {};
onProgress?.(10, 'Reading XML file...'); onProgress?.(10, 'Reading XML file...');
const xmlText = await file.text(); const rawXmlText = await file.text();
const xmlText = String(rawXmlText)
.replace(/<!DOCTYPE[\s\S]*?>/gi, '')
.replace(/<!ENTITY[\s\S]*?>/gi, '')
.replace(/<\?xml-stylesheet[\s\S]*?\?>/gi, '');
onProgress?.(30, 'Parsing XML structure...'); onProgress?.(30, 'Parsing XML structure...');
const parser = new DOMParser(); const parser = new XMLParser({
const xmlDoc = parser.parseFromString(xmlText, 'text/xml'); ignoreAttributes: false,
attributeNamePrefix: ATTR_PREFIX,
textNodeName: TEXT_KEY,
allowBooleanAttributes: true,
parseTagValue: false,
parseAttributeValue: false,
trimValues: true,
processEntities: true,
ignoreDeclaration: true,
ignorePiTags: true,
});
const parseError = xmlDoc.querySelector('parsererror'); let parsed: Record<string, unknown>;
if (parseError) { try {
throw new Error('Invalid XML: ' + parseError.textContent); parsed = parser.parse(xmlText) as Record<string, unknown>;
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
throw new Error('Invalid XML: ' + toSafeText(msg), { cause: err });
} }
const rootKeys = Object.keys(parsed);
if (rootKeys.length === 0) {
throw new Error('Invalid XML: no root element');
}
const rootName = rootKeys[0];
const rootValue = parsed[rootName];
onProgress?.(50, 'Analyzing data structure...'); onProgress?.(50, 'Analyzing data structure...');
const doc: jsPDFWithAutoTable = new jsPDF({ const doc: jsPDFWithAutoTable = new jsPDF({
orientation: 'landscape', orientation: 'landscape',
unit: 'mm', unit: 'mm',
format: 'a4' format: 'a4',
}); });
const pageWidth = doc.internal.pageSize.getWidth(); const pageWidth = doc.internal.pageSize.getWidth();
let yPosition = 20; let yPosition = 20;
const root = xmlDoc.documentElement;
const rootName = formatTitle(root.tagName);
doc.setFontSize(18); doc.setFontSize(18);
doc.setFont('helvetica', 'bold'); doc.setFont('helvetica', 'bold');
doc.text(rootName, pageWidth / 2, yPosition, { align: 'center' }); doc.text(formatTitle(rootName), pageWidth / 2, yPosition, {
align: 'center',
});
yPosition += 15; yPosition += 15;
onProgress?.(60, 'Generating formatted content...'); onProgress?.(60, 'Generating formatted content...');
const children = Array.from(root.children); if (isPlainObject(rootValue)) {
const rootObj = rootValue;
const childEntries = Object.entries(rootObj).filter(
([k]) => !k.startsWith(ATTR_PREFIX) && k !== TEXT_KEY
);
if (children.length > 0) { if (childEntries.length > 0) {
const groups = groupByTagName(children); const groups = groupChildrenByTagName(childEntries);
const renderableGroups: Array<[string, Record<string, unknown>[]]> = [];
for (const [groupName, elements] of Object.entries(groups)) { for (const [groupName, elements] of Object.entries(groups)) {
const { headers, rows } = extractTableData(elements); const { headers, rows } = extractTableData(elements);
if (headers.length > 0 && rows.length > 0) { if (headers.length > 0 && rows.length > 0) {
if (Object.keys(groups).length > 1) { renderableGroups.push([groupName, elements]);
}
}
for (const [groupName, elements] of renderableGroups) {
const { headers, rows } = extractTableData(elements);
if (renderableGroups.length > 1) {
doc.setFontSize(14); doc.setFontSize(14);
doc.setFont('helvetica', 'bold'); doc.setFont('helvetica', 'bold');
doc.text(formatTitle(groupName), 14, yPosition); doc.text(formatTitle(groupName), 14, yPosition);
@@ -65,7 +103,7 @@ export async function convertXmlToPdf(
} }
autoTable(doc, { autoTable(doc, {
head: [headers.map(h => formatTitle(h))], head: [headers.map((h) => formatTitle(h))],
body: rows, body: rows,
startY: yPosition, startY: yPosition,
styles: { styles: {
@@ -85,14 +123,13 @@ export async function convertXmlToPdf(
theme: 'striped', theme: 'striped',
didDrawPage: (data) => { didDrawPage: (data) => {
yPosition = (data.cursor?.y || yPosition) + 10; yPosition = (data.cursor?.y || yPosition) + 10;
} },
}); });
yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15; yPosition = (doc.lastAutoTable?.finalY || yPosition) + 15;
} }
}
} else { } else {
const kvPairs = extractKeyValuePairs(root); const kvPairs = extractKeyValuePairs(rootObj);
if (kvPairs.length > 0) { if (kvPairs.length > 0) {
autoTable(doc, { autoTable(doc, {
head: [['Property', 'Value']], head: [['Property', 'Value']],
@@ -116,6 +153,7 @@ export async function convertXmlToPdf(
}); });
} }
} }
}
onProgress?.(90, 'Finalizing PDF...'); onProgress?.(90, 'Finalizing PDF...');
@@ -125,40 +163,48 @@ export async function convertXmlToPdf(
return pdfBlob; return pdfBlob;
} }
function isPlainObject(val: unknown): val is Record<string, unknown> {
function groupByTagName(elements: Element[]): Record<string, Element[]> { return val !== null && typeof val === 'object' && !Array.isArray(val);
const groups: Record<string, Element[]> = {};
for (const element of elements) {
const tagName = element.tagName;
if (!groups[tagName]) {
groups[tagName] = [];
}
groups[tagName].push(element);
} }
function groupChildrenByTagName(
entries: [string, unknown][]
): Record<string, Record<string, unknown>[]> {
const groups: Record<string, Record<string, unknown>[]> = {};
for (const [tagName, value] of entries) {
const items = Array.isArray(value) ? value : [value];
const normalized: Record<string, unknown>[] = items.map((v) => {
if (isPlainObject(v)) return v;
if (v == null) return {};
return { [TEXT_KEY]: String(v) };
});
groups[tagName] = normalized;
}
return groups; return groups;
} }
function extractTableData(elements: Element[]): { headers: string[], rows: string[][] } { function extractTableData(elements: Record<string, unknown>[]): {
if (elements.length === 0) { headers: string[];
return { headers: [], rows: [] }; rows: string[][];
} } {
if (elements.length === 0) return { headers: [], rows: [] };
const headerSet = new Set<string>(); const headerSet = new Set<string>();
for (const element of elements) { for (const element of elements) {
for (const child of Array.from(element.children)) { for (const key of Object.keys(element)) {
headerSet.add(child.tagName); if (key.startsWith(ATTR_PREFIX)) continue;
if (key === TEXT_KEY) continue;
headerSet.add(key);
} }
} }
const headers = Array.from(headerSet); const headers = Array.from(headerSet);
if (headers.length === 0) return { headers: [], rows: [] };
const rows: string[][] = []; const rows: string[][] = [];
for (const element of elements) { for (const element of elements) {
const row: string[] = []; const row: string[] = [];
for (const header of headers) { for (const header of headers) {
const child = element.querySelector(header); row.push(toSafeText(stringifyValue(element[header])));
row.push(child?.textContent?.trim() || '');
} }
rows.push(row); rows.push(row);
} }
@@ -166,31 +212,69 @@ function extractTableData(elements: Element[]): { headers: string[], rows: strin
return { headers, rows }; return { headers, rows };
} }
function extractKeyValuePairs(obj: Record<string, unknown>): string[][] {
function extractKeyValuePairs(element: Element): string[][] {
const pairs: string[][] = []; const pairs: string[][] = [];
for (const child of Array.from(element.children)) { for (const [key, val] of Object.entries(obj)) {
const key = child.tagName; if (key.startsWith(ATTR_PREFIX)) continue;
const value = child.textContent?.trim() || ''; if (key === TEXT_KEY) continue;
if (value) { const strVal = toSafeText(stringifyValue(val));
pairs.push([formatTitle(key), value]); if (strVal) {
pairs.push([formatTitle(key), strVal]);
} }
} }
for (const attr of Array.from(element.attributes)) { for (const [key, val] of Object.entries(obj)) {
pairs.push([formatTitle(attr.name), attr.value]); if (!key.startsWith(ATTR_PREFIX)) continue;
const attrName = key.slice(ATTR_PREFIX.length);
pairs.push([formatTitle(attrName), toSafeText(stringifyValue(val))]);
} }
return pairs; return pairs;
} }
function stringifyValue(val: unknown): string {
if (val == null) return '';
if (typeof val === 'string') return val;
if (typeof val === 'number' || typeof val === 'boolean') return String(val);
if (Array.isArray(val)) {
return val
.map((v) => stringifyValue(v))
.filter((s) => s.length > 0)
.join(', ');
}
if (isPlainObject(val)) {
const parts: string[] = [];
if (TEXT_KEY in val) {
const t = stringifyValue(val[TEXT_KEY]);
if (t) parts.push(t);
}
for (const [k, v] of Object.entries(val)) {
if (k.startsWith(ATTR_PREFIX)) continue;
if (k === TEXT_KEY) continue;
const inner = stringifyValue(v);
if (inner) parts.push(inner);
}
return parts.join(' ');
}
return String(val);
}
function toSafeText(raw: string | null | undefined): string {
if (raw == null) return '';
return (
String(raw)
// eslint-disable-next-line no-control-regex
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
.trim()
);
}
function formatTitle(tagName: string): string { function formatTitle(tagName: string): string {
return tagName return tagName
.replace(/[_-]/g, ' ') .replace(/[_-]/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2') .replace(/([a-z])([A-Z])/g, '$1 $2')
.split(' ') .split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' '); .join(' ');
} }