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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user