security: fix all Critical and High findings from FLAWS_REPORT audit
- Auth: pessimistic locking on login tokens and refresh token rotation, backup code attempt counter, rate limiting verification - Schema: unique constraints on business numbers, FK relations, unsigned/signed alignment, attendance duplicate prevention - Invoices/PDFs: DOMPurify sanitization, bounded queries in stats and alerts, VAT rounding, Puppeteer error handling - Orders/Offers: transactional parent+child creation, Zod NaN refinement, status enums, uniqueness checks - Projects/Files: path traversal protection, streamed uploads, permission guards, query param validation - Attendance/HR: duplicate checks, ownership validation, GPS restrictions, trip distance validation - Frontend: modal lock reference counting, XSS escaping in print HTML, ref mutation fixes, accessibility attributes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,12 @@ import { requirePermission } from "../../middleware/auth";
|
||||
import { localDateCzStr } from "../../utils/date";
|
||||
import { nasOffersManager } from "../../services/nas-offers-manager";
|
||||
import { htmlToPdf } from "../../utils/html-to-pdf";
|
||||
import { parseId } from "../../utils/response";
|
||||
import createDOMPurify from "dompurify";
|
||||
import { JSDOM } from "jsdom";
|
||||
|
||||
const window = new JSDOM("").window;
|
||||
const DOMPurify = createDOMPurify(window);
|
||||
|
||||
function formatDate(date: Date | string | null | undefined): string {
|
||||
if (!date) return "";
|
||||
@@ -73,6 +79,7 @@ function cleanQuillHtml(html: string | null | undefined): string {
|
||||
s = s.replace(/href\s*=\s*["']?\s*javascript\s*:[^"'>\s]*/gi, 'href="#"');
|
||||
// Replace with regular space (outside of tags)
|
||||
s = s.replace(/( )/g, " ");
|
||||
s = s.replace(/\s+style\s*=\s*("[^"]*"|'[^']*'|[^\s>]*)/gi, "");
|
||||
// Merge adjacent spans with same attributes
|
||||
let prev = "";
|
||||
while (prev !== s) {
|
||||
@@ -102,7 +109,12 @@ function buildAddressLines(
|
||||
let fieldOrder: string[] | null = null;
|
||||
const raw = entity.custom_fields;
|
||||
if (raw) {
|
||||
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
if (parsed && typeof parsed === "object") {
|
||||
if ((parsed as Record<string, unknown>).fields) {
|
||||
cfData =
|
||||
@@ -201,7 +213,8 @@ export default async function offersPdfRoutes(
|
||||
"/:id",
|
||||
{ preHandler: requirePermission("offers.view") },
|
||||
async (request, reply) => {
|
||||
const id = parseInt(request.params.id, 10);
|
||||
const id = parseId(request.params.id, reply);
|
||||
if (id === null) return;
|
||||
const query = request.query as Record<string, string>;
|
||||
|
||||
try {
|
||||
@@ -349,7 +362,7 @@ export default async function offersPdfRoutes(
|
||||
if (title)
|
||||
scopeHtml += `<div class="scope-section-title">${escapeHtml(title)}</div>`;
|
||||
if (content)
|
||||
scopeHtml += `<div class="section-content">${cleanQuillHtml(content)}</div>`;
|
||||
scopeHtml += `<div class="section-content">${cleanQuillHtml(DOMPurify.sanitize(content))}</div>`;
|
||||
scopeHtml += "</div>";
|
||||
}
|
||||
scopeHtml += "</div>";
|
||||
@@ -761,28 +774,24 @@ ${indentCSS}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
const saveMode = query.save === "1";
|
||||
|
||||
// Save PDF to NAS
|
||||
if (nasOffersManager.isConfigured() && quotation.quotation_number) {
|
||||
if (
|
||||
saveMode &&
|
||||
nasOffersManager.isConfigured() &&
|
||||
quotation.quotation_number
|
||||
) {
|
||||
const created = quotation.created_at
|
||||
? new Date(quotation.created_at)
|
||||
: new Date();
|
||||
const saveMode = query.save === "1";
|
||||
const pdfPromise = htmlToPdf(html)
|
||||
.then((pdfBuffer) => {
|
||||
nasOffersManager.saveOfferPdf(
|
||||
quotation.quotation_number!,
|
||||
created.getFullYear(),
|
||||
pdfBuffer,
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
request.log.error(err, "Failed to save offer PDF to NAS");
|
||||
});
|
||||
|
||||
if (saveMode) {
|
||||
await pdfPromise;
|
||||
return reply.send({ success: true, message: "PDF uloženo" });
|
||||
}
|
||||
const pdfBuffer = await htmlToPdf(html);
|
||||
nasOffersManager.saveOfferPdf(
|
||||
quotation.quotation_number!,
|
||||
created.getFullYear(),
|
||||
pdfBuffer,
|
||||
);
|
||||
return reply.send({ success: true, message: "PDF uloženo" });
|
||||
}
|
||||
|
||||
return reply.type("text/html").send(html);
|
||||
|
||||
Reference in New Issue
Block a user