security: fix all Medium findings from FLAWS_REPORT audit

- Auth: TOTP replay protection with counter tracking, constant-time
  backup code comparison, atomic lockout increment, per-token logout
- Invoices/PDFs: net-based VAT calculation, dangerous URL scheme
  stripping in cleanQuillHtml, orders-pdf error handling
- Orders: reject item changes on status transition, cascading
  delete cleanup, take:1 with orderBy
- Projects: atomic rename collision handling, MIME/extension
  validation, empty customer name rejection
- Attendance: Czech public holiday awareness in frontend fund
  calculation, leave_hours 0 handling, invalid date NaN guard,
  bounded per-month queries in workfund
- Users/Admin: profile audit logging + password validation, session
  revocation guard, session ID validation, dashboard DB aggregation,
  soft-deleted record protection in scope templates
- Frontend: FormField label linkage, Pagination ARIA, error
  handling in OrderConfirmationModal, 401 propagation, GPS emoji
  hidden from screen readers, table sort state fix, geolocation
  race/abort cleanup, Leaflet popup DOM safety, Vehicles toggleActive
  minimal body, CompanySettings ref mutation fix, OfferDetail unlock
  abort, AttendanceBalances combined fetches
- Utils: env validation, Puppeteer concurrency mutex, invoice alert
  cron cleanup on shutdown, body limit alignment, TOTP error logging,
  trustProxy from env, symlink rejection, rate cache Map usage

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-04-24 08:24:14 +02:00
parent 528e55991b
commit 4f4b12f039
33 changed files with 442 additions and 211 deletions

View File

@@ -1,31 +1,41 @@
import { Browser } from "puppeteer";
let browser: Browser | null = null;
let launching: Promise<Browser> | null = null;
async function getBrowser(): Promise<Browser> {
if (browser && browser.connected) return browser;
// Try puppeteer (bundles Chromium), fall back to puppeteer-core (system Chromium)
try {
const puppeteer = await import("puppeteer");
browser = await puppeteer.default.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
});
} catch {
const core = await import("puppeteer-core");
const executablePath =
process.env.CHROMIUM_PATH ||
"/usr/bin/chromium-browser" ||
"/usr/bin/chromium";
browser = await core.default.launch({
headless: true,
executablePath,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
});
}
if (launching) return launching;
return browser;
launching = (async () => {
// Try puppeteer (bundles Chromium), fall back to puppeteer-core (system Chromium)
try {
const puppeteer = await import("puppeteer");
browser = await puppeteer.default.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
});
} catch {
const core = await import("puppeteer-core");
const executablePath =
process.env.CHROMIUM_PATH ||
"/usr/bin/chromium-browser" ||
"/usr/bin/chromium";
browser = await core.default.launch({
headless: true,
executablePath,
args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"],
});
}
return browser!;
})();
try {
return await launching;
} finally {
launching = null;
}
}
export async function htmlToPdf(html: string): Promise<Buffer> {

View File

@@ -2,7 +2,10 @@ import * as OTPAuthLib from "otpauth";
import { decrypt } from "./encryption";
export const OTPAuth = {
verify(encryptedSecret: string, code: string): boolean {
verify(
encryptedSecret: string,
code: string,
): { valid: boolean; counter: number | null } {
try {
const secret = decrypt(encryptedSecret);
const totp = new OTPAuthLib.TOTP({
@@ -12,9 +15,14 @@ export const OTPAuth = {
period: 30,
});
const delta = totp.validate({ token: code, window: 1 });
return delta !== null;
} catch {
return false;
if (delta === null) {
return { valid: false, counter: null };
}
const currentCounter = Math.floor(Date.now() / 1000 / 30);
return { valid: true, counter: currentCounter + delta };
} catch (err) {
console.error("TOTP verification error:", err);
return { valid: false, counter: null };
}
},
};