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:
BOHA
2026-04-24 00:58:35 +02:00
parent 122eee175e
commit 528e55991b
57 changed files with 2355 additions and 1010 deletions

View File

@@ -1,5 +1,6 @@
import fs from "fs";
import path from "path";
import { pipeline } from "stream/promises";
import { config } from "../config/env";
import { localDateStr, localTimeStr } from "../utils/date";
@@ -294,21 +295,21 @@ export class NasFileManager {
public async uploadFile(
projectNumber: string,
subPath: string,
fileBuffer: Buffer,
fileStream: NodeJS.ReadableStream,
fileName: string,
): Promise<string | null> {
const dirPath = this.resolveProjectPath(projectNumber, subPath);
if (
dirPath === null ||
!fs.existsSync(dirPath) ||
!fs.statSync(dirPath).isDirectory()
) {
if (dirPath === null) {
return "Cílová složka neexistuje";
}
if (fileBuffer.length > config.nas.maxUploadSize) {
const maxMb = Math.round(config.nas.maxUploadSize / 1048576);
return `Soubor je příliš velký (max ${maxMb} MB)`;
try {
const stat = await fs.promises.stat(dirPath);
if (!stat.isDirectory()) {
return "Cílová složka neexistuje";
}
} catch {
return "Cílová složka neexistuje";
}
const originalName = path.basename(fileName);
@@ -322,9 +323,22 @@ export class NasFileManager {
return "Tento typ souboru není povolen";
}
const tempPath = path.join(
require("os").tmpdir(),
`upload-${Date.now()}-${Math.random().toString(36).slice(2)}`,
);
try {
const typeResult = await FileType.fromBuffer(fileBuffer);
await pipeline(fileStream, fs.createWriteStream(tempPath));
} catch {
await fs.promises.unlink(tempPath).catch(() => {});
return "Nepodařilo se uložit soubor";
}
try {
const typeResult = await FileType.fromFile(tempPath);
if (typeResult && this.isSuspiciousMime(typeResult.mime, ext)) {
await fs.promises.unlink(tempPath).catch(() => {});
return "Obsah souboru neodpovídá jeho příponě";
}
} catch {
@@ -333,19 +347,28 @@ export class NasFileManager {
let destPath = dirPath + "/" + safeName;
if (fs.existsSync(destPath)) {
try {
await fs.promises.stat(destPath);
const base = path.basename(safeName, ext ? "." + ext : "");
let counter = 1;
do {
safeName = base + "_" + counter + (ext ? "." + ext : "");
destPath = dirPath + "/" + safeName;
counter++;
} while (fs.existsSync(destPath));
} while (
await fs.promises
.stat(destPath)
.then(() => true)
.catch(() => false)
);
} catch {
// destPath does not exist, continue
}
try {
fs.writeFileSync(destPath, fileBuffer);
await fs.promises.rename(tempPath, destPath);
} catch {
await fs.promises.unlink(tempPath).catch(() => {});
return "Nepodařilo se uložit soubor";
}
@@ -381,7 +404,12 @@ export class NasFileManager {
projectNumber: string,
filePath: string,
): Promise<string | null> {
if (filePath === "" || filePath === "/") {
if (
filePath === "" ||
filePath === "/" ||
filePath === "." ||
filePath === "./"
) {
return "Nelze smazat kořenovou složku projektu";
}
@@ -390,22 +418,19 @@ export class NasFileManager {
return "Neplatná cesta";
}
if (!fs.existsSync(fullPath)) {
return "Soubor nebo složka neexistuje";
}
let isDir: boolean;
try {
isDir = fs.lstatSync(fullPath).isDirectory();
const stat = await fs.promises.lstat(fullPath);
isDir = stat.isDirectory();
} catch {
return "Neplatná cesta";
return "Soubor nebo složka neexistuje";
}
try {
if (isDir) {
await fs.promises.rm(fullPath, { recursive: true, force: true });
} else {
fs.unlinkSync(fullPath);
await fs.promises.unlink(fullPath);
}
} catch {
return isDir
@@ -416,12 +441,17 @@ export class NasFileManager {
return null;
}
public moveItem(
public async moveItem(
projectNumber: string,
fromPath: string,
toPath: string,
): string | null {
if (fromPath === "" || fromPath === "/") {
): Promise<string | null> {
if (
fromPath === "" ||
fromPath === "/" ||
fromPath === "." ||
fromPath === "./"
) {
return "Nelze přesunout kořenovou složku";
}
@@ -432,7 +462,9 @@ export class NasFileManager {
return "Neplatná cesta";
}
if (!fs.existsSync(fullFrom)) {
try {
await fs.promises.stat(fullFrom);
} catch {
return "Zdrojový soubor neexistuje";
}
@@ -441,8 +473,13 @@ export class NasFileManager {
fullFrom.replace(/\\/g, "/").toLowerCase() ===
fullTo.replace(/\\/g, "/").toLowerCase();
if (fs.existsSync(fullTo) && !sameFile) {
return "Cílový soubor již existuje";
if (!sameFile) {
try {
await fs.promises.stat(fullTo);
return "Cílový soubor již existuje";
} catch {
// target does not exist, continue
}
}
const targetName = path.basename(toPath);
@@ -451,7 +488,7 @@ export class NasFileManager {
}
try {
fs.renameSync(fullFrom, fullTo);
await fs.promises.rename(fullFrom, fullTo);
} catch (err: unknown) {
if (
err instanceof Error &&
@@ -466,17 +503,22 @@ export class NasFileManager {
return null;
}
public createFolder(
public async createFolder(
projectNumber: string,
subPath: string,
folderName: string,
): string | null {
): Promise<string | null> {
const dirPath = this.resolveProjectPath(projectNumber, subPath);
if (
dirPath === null ||
!fs.existsSync(dirPath) ||
!fs.statSync(dirPath).isDirectory()
) {
if (dirPath === null) {
return "Nadřazená složka neexistuje";
}
try {
const stat = await fs.promises.stat(dirPath);
if (!stat.isDirectory()) {
return "Nadřazená složka neexistuje";
}
} catch {
return "Nadřazená složka neexistuje";
}
@@ -486,12 +528,15 @@ export class NasFileManager {
}
const newPath = dirPath + "/" + safeName;
if (fs.existsSync(newPath)) {
try {
await fs.promises.stat(newPath);
return "Složka s tímto názvem již existuje";
} catch {
// does not exist, continue
}
try {
fs.mkdirSync(newPath, { mode: 0o775 });
await fs.promises.mkdir(newPath, { mode: 0o775 });
} catch {
return "Nepodařilo se vytvořit složku";
}
@@ -572,6 +617,11 @@ export class NasFileManager {
}
const normalized = subPath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
// Reject explicit current-directory references (defense-in-depth for destructive ops)
if (normalized === "." || normalized === "./") {
return null;
}
const candidate = path.resolve(folderPath, normalized).replace(/\\/g, "/");
// Verify candidate is within project folder