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:
@@ -224,11 +224,20 @@ function computeUserTotals(
|
||||
// Print helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function renderFundStatus(userData: Record<string, any>): string {
|
||||
if (userData.overtime > 0)
|
||||
return `<span class="leave-badge badge-overtime">+${userData.overtime}h přesčas</span>`;
|
||||
return `<span class="leave-badge badge-overtime">+${escapeHtml(String(userData.overtime))}h přesčas</span>`;
|
||||
if (userData.missing > 0)
|
||||
return `<span style="color:#dc2626">−${userData.missing}h</span>`;
|
||||
return `<span style="color:#dc2626">−${escapeHtml(String(userData.missing))}h</span>`;
|
||||
return '<span style="color:#16a34a">splněno</span>';
|
||||
}
|
||||
|
||||
@@ -255,11 +264,11 @@ function buildProjectLogsHtml(record: Record<string, any>): string {
|
||||
h = 0;
|
||||
m = 0;
|
||||
}
|
||||
return `<div>${log.project_name || `#${log.project_id}`} (${h}:${String(m).padStart(2, "0")}h)</div>`;
|
||||
return `<div>${escapeHtml(log.project_name || `#${log.project_id}`)} (${h}:${String(m).padStart(2, "0")}h)</div>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
return record.project_name || "—";
|
||||
return escapeHtml(record.project_name || "—");
|
||||
}
|
||||
|
||||
function buildLeaveSummaryHtml(
|
||||
@@ -268,15 +277,15 @@ function buildLeaveSummaryHtml(
|
||||
printData: Record<string, any>,
|
||||
): string {
|
||||
const bal = printData.leave_balances[userId];
|
||||
let parts = `<strong>Dovolená ${printData.year}:</strong> Zbývá ${bal.vacation_remaining.toFixed(1)}h z ${bal.vacation_total}h`;
|
||||
let parts = `<strong>Dovolená ${escapeHtml(String(printData.year))}:</strong> Zbývá ${bal.vacation_remaining.toFixed(1)}h z ${bal.vacation_total}h`;
|
||||
if (userData.vacation_hours > 0)
|
||||
parts += ` | <span class="leave-badge badge-vacation">Tento měsíc: ${userData.vacation_hours}h</span>`;
|
||||
parts += ` | <span class="leave-badge badge-vacation">Tento měsíc: ${escapeHtml(String(userData.vacation_hours))}h</span>`;
|
||||
if (userData.sick_hours > 0)
|
||||
parts += ` | <span class="leave-badge badge-sick">Nemoc: ${userData.sick_hours}h</span>`;
|
||||
parts += ` | <span class="leave-badge badge-sick">Nemoc: ${escapeHtml(String(userData.sick_hours))}h</span>`;
|
||||
if (userData.holiday_hours > 0)
|
||||
parts += ` | <span class="leave-badge badge-holiday">Svátek: ${userData.holiday_hours}h</span>`;
|
||||
parts += ` | <span class="leave-badge badge-holiday">Svátek: ${escapeHtml(String(userData.holiday_hours))}h</span>`;
|
||||
if (userData.overtime > 0)
|
||||
parts += ` | <span class="leave-badge badge-overtime">Přesčas: +${userData.overtime}h</span>`;
|
||||
parts += ` | <span class="leave-badge badge-overtime">Přesčas: +${escapeHtml(String(userData.overtime))}h</span>`;
|
||||
return `<div class="leave-summary">${parts}</div>`;
|
||||
}
|
||||
|
||||
@@ -299,17 +308,17 @@ function buildUserSectionHtml(
|
||||
const breakCell =
|
||||
isLeave || !record.break_start || !record.break_end
|
||||
? "—"
|
||||
: `${formatTimeOrDatetimePrint(record.break_start, record.shift_date)} - ${formatTimeOrDatetimePrint(record.break_end, record.shift_date)}`;
|
||||
: `${escapeHtml(formatTimeOrDatetimePrint(record.break_start, record.shift_date))} - ${escapeHtml(formatTimeOrDatetimePrint(record.break_end, record.shift_date))}`;
|
||||
|
||||
return `<tr>
|
||||
<td>${formatDate(record.shift_date)}</td>
|
||||
<td><span class="leave-badge ${getLeaveTypeBadgeClass(leaveType)}">${getLeaveTypeName(leaveType)}</span></td>
|
||||
<td class="text-center">${isLeave ? "—" : formatTimeOrDatetimePrint(record.arrival_time, record.shift_date)}</td>
|
||||
<td>${escapeHtml(formatDate(record.shift_date))}</td>
|
||||
<td><span class="leave-badge ${escapeHtml(getLeaveTypeBadgeClass(leaveType))}">${escapeHtml(getLeaveTypeName(leaveType))}</span></td>
|
||||
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.arrival_time, record.shift_date))}</td>
|
||||
<td class="text-center">${breakCell}</td>
|
||||
<td class="text-center">${isLeave ? "—" : formatTimeOrDatetimePrint(record.departure_time, record.shift_date)}</td>
|
||||
<td class="text-center">${isLeave ? "—" : escapeHtml(formatTimeOrDatetimePrint(record.departure_time, record.shift_date))}</td>
|
||||
<td class="text-center">${workMinutes > 0 ? `${hours}:${String(mins).padStart(2, "0")}` : "—"}</td>
|
||||
<td style="font-size:8px">${buildProjectLogsHtml(record)}</td>
|
||||
<td>${record.notes || ""}</td>
|
||||
<td>${escapeHtml(record.notes || "")}</td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
@@ -318,15 +327,15 @@ function buildUserSectionHtml(
|
||||
userData.fund !== null
|
||||
? `<tr>
|
||||
<td colspan="6" class="text-right">Fond měsíce:</td>
|
||||
<td class="text-center">${userData.covered}h / ${userData.fund}h</td>
|
||||
<td class="text-center">${escapeHtml(String(userData.covered))}h / ${escapeHtml(String(userData.fund))}h</td>
|
||||
<td colspan="2">${renderFundStatus(userData)}</td>
|
||||
</tr>`
|
||||
: "";
|
||||
|
||||
return `<div class="user-section">
|
||||
<div class="user-header">
|
||||
<h3>${userData.name}</h3>
|
||||
<span class="total">Odpracováno: ${formatMinutes(userData.minutes)} h</span>
|
||||
<h3>${escapeHtml(userData.name)}</h3>
|
||||
<span class="total">Odpracováno: ${escapeHtml(formatMinutes(userData.minutes))} h</span>
|
||||
</div>
|
||||
${leaveHtml}
|
||||
<table>
|
||||
@@ -344,7 +353,7 @@ function buildUserSectionHtml(
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="6" class="text-right">Odpracováno:</td>
|
||||
<td class="text-center">${formatMinutes(userData.minutes)} h</td>
|
||||
<td class="text-center">${escapeHtml(formatMinutes(userData.minutes))} h</td>
|
||||
<td colspan="2"></td>
|
||||
</tr>
|
||||
${fundRow}
|
||||
@@ -365,7 +374,7 @@ function buildPrintHtml(
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Docházka - ${pData.month_name}</title>
|
||||
<title>Docházka - ${escapeHtml(pData.month_name)}</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
@@ -428,11 +437,11 @@ function buildPrintHtml(
|
||||
<img src="/api/admin/company-settings/logo?variant=light" alt="" class="print-logo" />
|
||||
<div class="print-header-text">
|
||||
<h1>EVIDENCE DOCHÁZKY</h1>
|
||||
<div class="company">${companyName}</div>
|
||||
<div class="company">${escapeHtml(companyName)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="print-header-right">
|
||||
<div class="period">${pData.month_name}</div>
|
||||
<div class="period">${escapeHtml(pData.month_name)}</div>
|
||||
${filterNote}
|
||||
<div class="generated">Vygenerováno: ${new Date().toLocaleString("cs-CZ")}</div>
|
||||
</div>
|
||||
@@ -1037,7 +1046,7 @@ export default function useAttendanceAdmin({ alert }: AlertContext) {
|
||||
? '<p style="text-align:center;padding:20px">Za vybrané období nejsou žádné záznamy.</p>'
|
||||
: "";
|
||||
const filterNote = pData.selected_user_name
|
||||
? `<div class="filters">Zaměstnanec: ${pData.selected_user_name}</div>`
|
||||
? `<div class="filters">Zaměstnanec: ${escapeHtml(pData.selected_user_name)}</div>`
|
||||
: "";
|
||||
const bodyContent = buildPrintHtml(
|
||||
pData,
|
||||
|
||||
Reference in New Issue
Block a user