From 106606f3fa46ec9884740395ccd7d7298638d98f Mon Sep 17 00:00:00 2001 From: BOHA Date: Tue, 24 Mar 2026 20:13:20 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20code=20review=20=E2=80=94=20XSS,=20type?= =?UTF-8?q?=20safety,=20validation=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - InvoiceDetail: sanitize notes HTML with DOMPurify - OrderDetail: use proper DOMPurify import instead of window fallback Important: - AttendanceBalances: add fund_to_date to interface, remove as-any casts - All schemas: replace z.any() with z.preprocess for boolean fields - Routes: simplify boolean coercion (Zod handles it now) Co-Authored-By: Claude Opus 4.6 (1M context) --- package-lock.json | 28 ++++++++++++++++++++++++++ package.json | 2 ++ src/admin/pages/AttendanceBalances.tsx | 10 ++++----- src/admin/pages/InvoiceDetail.tsx | 5 ++++- src/admin/pages/OrderDetail.tsx | 4 +--- src/routes/admin/bank-accounts.ts | 8 ++------ src/routes/admin/company-settings.ts | 6 +----- src/routes/admin/trips.ts | 8 ++------ src/routes/admin/vehicles.ts | 4 +--- src/schemas/bank-accounts.schema.ts | 4 ++-- src/schemas/company-settings.schema.ts | 2 +- src/schemas/invoices.schema.ts | 4 ++-- src/schemas/offers.schema.ts | 6 +++--- src/schemas/orders.schema.ts | 6 +++--- src/schemas/trips.schema.ts | 4 ++-- src/schemas/users.schema.ts | 4 ++-- src/schemas/vehicles.schema.ts | 4 ++-- 17 files changed, 63 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2b6659..a863c99 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@prisma/client": "^6.19.2", "bcryptjs": "^3.0.3", "date-fns": "^4.1.0", + "dompurify": "^3.3.3", "dotenv": "^17.3.1", "fastify": "^5.8.2", "file-type": "^16.5.4", @@ -40,6 +41,7 @@ }, "devDependencies": { "@types/bcryptjs": "^2.4.6", + "@types/dompurify": "^3.0.5", "@types/jsonwebtoken": "^9.0.10", "@types/mysql": "^2.15.27", "@types/node": "^25.5.0", @@ -1432,6 +1434,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1548,6 +1560,13 @@ "@types/superagent": "^8.1.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", @@ -2314,6 +2333,15 @@ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "17.3.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", diff --git a/package.json b/package.json index 7874f7c..9675de9 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@prisma/client": "^6.19.2", "bcryptjs": "^3.0.3", "date-fns": "^4.1.0", + "dompurify": "^3.3.3", "dotenv": "^17.3.1", "fastify": "^5.8.2", "file-type": "^16.5.4", @@ -55,6 +56,7 @@ }, "devDependencies": { "@types/bcryptjs": "^2.4.6", + "@types/dompurify": "^3.0.5", "@types/jsonwebtoken": "^9.0.10", "@types/mysql": "^2.15.27", "@types/node": "^25.5.0", diff --git a/src/admin/pages/AttendanceBalances.tsx b/src/admin/pages/AttendanceBalances.tsx index 46104ff..d4303f9 100644 --- a/src/admin/pages/AttendanceBalances.tsx +++ b/src/admin/pages/AttendanceBalances.tsx @@ -34,6 +34,7 @@ interface FundUserData { interface MonthFundData { month_name: string; fund: number; + fund_to_date: number; business_days: number; users?: Record; } @@ -320,7 +321,7 @@ export default function AttendanceBalances() { let totalCovered = 0; for (const monthData of Object.values(fundData.months)) { // Use prorated fund (fund_to_date) for current month, full fund for past - totalFund += (monthData as any).fund_to_date ?? monthData.fund; + totalFund += monthData.fund_to_date ?? monthData.fund; const us = monthData.users?.[userId]; if (us) { totalWorked += us.worked; @@ -571,10 +572,9 @@ export default function AttendanceBalances() { className="text-secondary" style={{ fontSize: "12px" }} > - {(monthData as any).fund_to_date ?? monthData.fund}h ( + {monthData.fund_to_date ?? monthData.fund}h ( {Math.round( - ((monthData as any).fund_to_date ?? - monthData.fund) / 8, + (monthData.fund_to_date ?? monthData.fund) / 8, )}{" "} dnů) @@ -591,7 +591,7 @@ export default function AttendanceBalances() { const us = monthData.users?.[String(user.id)]; if (!us) return null; const effectiveFund = - (monthData as any).fund_to_date ?? monthData.fund; + monthData.fund_to_date ?? monthData.fund; const pct = effectiveFund > 0 ? Math.min( diff --git a/src/admin/pages/InvoiceDetail.tsx b/src/admin/pages/InvoiceDetail.tsx index 82a602a..65e91c3 100644 --- a/src/admin/pages/InvoiceDetail.tsx +++ b/src/admin/pages/InvoiceDetail.tsx @@ -5,6 +5,7 @@ import { useParams, Link, } from "react-router-dom"; +import DOMPurify from "dompurify"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import Forbidden from "../components/Forbidden"; @@ -1969,7 +1970,9 @@ export default function InvoiceDetail() {

Veřejné poznámky na faktuře

{isPaid ? ( notes && notes.trim() && notes !== "


" ? ( -
+
) : (

Žádné poznámky.

) diff --git a/src/admin/pages/OrderDetail.tsx b/src/admin/pages/OrderDetail.tsx index 45c183b..232e9c5 100644 --- a/src/admin/pages/OrderDetail.tsx +++ b/src/admin/pages/OrderDetail.tsx @@ -5,9 +5,7 @@ import { useMemo, type ReactNode, } from "react"; -const DOMPurify = (window as any).DOMPurify || { - sanitize: (html: string) => html, -}; +import DOMPurify from "dompurify"; import { useAlert } from "../context/AlertContext"; import { useAuth } from "../context/AuthContext"; import { useParams, useNavigate, Link } from "react-router-dom"; diff --git a/src/routes/admin/bank-accounts.ts b/src/routes/admin/bank-accounts.ts index dc24153..3c7b147 100644 --- a/src/routes/admin/bank-accounts.ts +++ b/src/routes/admin/bank-accounts.ts @@ -41,9 +41,7 @@ export default async function bankAccountsRoutes( bic: body.bic ? String(body.bic) : null, currency: body.currency ? String(body.currency) : "CZK", is_default: - body.is_default === true || - body.is_default === 1 || - body.is_default === "1", + !!body.is_default, position: body.position ? Number(body.position) : 0, }, }); @@ -110,9 +108,7 @@ export default async function bankAccountsRoutes( body.currency !== undefined ? String(body.currency) : undefined, is_default: body.is_default !== undefined - ? body.is_default === true || - body.is_default === 1 || - body.is_default === "1" + ? !!body.is_default : undefined, position: body.position !== undefined ? Number(body.position) : undefined, diff --git a/src/routes/admin/company-settings.ts b/src/routes/admin/company-settings.ts index b2acf65..cdd342c 100644 --- a/src/routes/admin/company-settings.ts +++ b/src/routes/admin/company-settings.ts @@ -215,11 +215,7 @@ export default async function companySettingsRoutes( } if (body.default_vat_rate !== undefined) data.default_vat_rate = Number(body.default_vat_rate); - if (body.require_2fa !== undefined) - data.require_2fa = - body.require_2fa === true || - body.require_2fa === 1 || - body.require_2fa === "1"; + if (body.require_2fa !== undefined) data.require_2fa = !!body.require_2fa; if ( body.custom_fields !== undefined || body.supplier_field_order !== undefined diff --git a/src/routes/admin/trips.ts b/src/routes/admin/trips.ts index 50cc5e8..b81b237 100644 --- a/src/routes/admin/trips.ts +++ b/src/routes/admin/trips.ts @@ -177,9 +177,7 @@ export default async function tripsRoutes( route_from: String(body.route_from), route_to: String(body.route_to), is_business: - body.is_business === true || - body.is_business === 1 || - body.is_business === "1", + !!body.is_business, notes: body.notes ? String(body.notes) : null, }, }); @@ -231,9 +229,7 @@ export default async function tripsRoutes( if (body.route_to !== undefined) data.route_to = String(body.route_to); if (body.is_business !== undefined) data.is_business = - body.is_business === true || - body.is_business === 1 || - body.is_business === "1"; + !!body.is_business; if (body.notes !== undefined) data.notes = body.notes ? String(body.notes) : null; diff --git a/src/routes/admin/vehicles.ts b/src/routes/admin/vehicles.ts index 5c41410..33bcd9d 100644 --- a/src/routes/admin/vehicles.ts +++ b/src/routes/admin/vehicles.ts @@ -108,9 +108,7 @@ export default async function vehiclesRoutes( body.actual_km !== undefined ? Number(body.actual_km) : undefined, is_active: body.is_active !== undefined - ? body.is_active === true || - body.is_active === 1 || - body.is_active === "1" + ? !!body.is_active : undefined, }, }); diff --git a/src/schemas/bank-accounts.schema.ts b/src/schemas/bank-accounts.schema.ts index 1bb47e2..aa80883 100644 --- a/src/schemas/bank-accounts.schema.ts +++ b/src/schemas/bank-accounts.schema.ts @@ -7,7 +7,7 @@ export const CreateBankAccountSchema = z.object({ iban: z.string().nullish(), bic: z.string().nullish(), currency: z.string().optional().default("CZK"), - is_default: z.any().optional().default(false), + is_default: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(false), position: z .union([z.number(), z.string()]) .transform((v) => Number(v)) @@ -22,7 +22,7 @@ export const UpdateBankAccountSchema = z.object({ iban: z.string().nullish(), bic: z.string().nullish(), currency: z.string().optional(), - is_default: z.any().optional(), + is_default: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), position: z .union([z.number(), z.string()]) .transform((v) => Number(v)) diff --git a/src/schemas/company-settings.schema.ts b/src/schemas/company-settings.schema.ts index 47e6368..a14eeaa 100644 --- a/src/schemas/company-settings.schema.ts +++ b/src/schemas/company-settings.schema.ts @@ -16,7 +16,7 @@ export const UpdateCompanySettingsSchema = z.object({ .union([z.number(), z.string()]) .transform((v) => Number(v)) .optional(), - require_2fa: z.any().optional(), + require_2fa: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), custom_fields: z.array(z.any()).optional(), supplier_field_order: z.array(z.any()).optional(), }); diff --git a/src/schemas/invoices.schema.ts b/src/schemas/invoices.schema.ts index 19c0cb6..adf9a41 100644 --- a/src/schemas/invoices.schema.ts +++ b/src/schemas/invoices.schema.ts @@ -41,7 +41,7 @@ export const CreateInvoiceSchema = z.object({ .transform((v) => Number(v)) .optional() .default(21.0), - apply_vat: z.any().optional().default(true), + apply_vat: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(true), payment_method: z.string().nullish(), constant_symbol: z.string().nullish(), bank_name: z.string().nullish(), @@ -73,7 +73,7 @@ export const UpdateInvoiceSchema = z.object({ .union([z.number(), z.string()]) .transform((v) => Number(v)) .optional(), - apply_vat: z.any().optional(), + apply_vat: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), issue_date: z.union([z.string(), z.null()]).optional(), due_date: z.union([z.string(), z.null()]).optional(), tax_date: z.union([z.string(), z.null()]).optional(), diff --git a/src/schemas/offers.schema.ts b/src/schemas/offers.schema.ts index fcb7f05..5d33a92 100644 --- a/src/schemas/offers.schema.ts +++ b/src/schemas/offers.schema.ts @@ -14,7 +14,7 @@ const QuotationItemSchema = z.object({ .transform((v) => Number(v) || 0) .optional() .default(0), - is_included_in_total: z.any().optional().default(true), + is_included_in_total: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(true), position: z .union([z.number(), z.string()]) .transform((v) => Number(v)) @@ -46,7 +46,7 @@ export const CreateQuotationSchema = z.object({ .transform((v) => Number(v)) .optional() .default(21.0), - apply_vat: z.any().optional().default(true), + apply_vat: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(true), exchange_rate: z .union([z.number(), z.string()]) .transform((v) => Number(v)) @@ -73,7 +73,7 @@ export const UpdateQuotationSchema = z.object({ .union([z.number(), z.string()]) .transform((v) => Number(v)) .optional(), - apply_vat: z.any().optional(), + apply_vat: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), exchange_rate: z .union([z.number(), z.string()]) .transform((v) => Number(v)) diff --git a/src/schemas/orders.schema.ts b/src/schemas/orders.schema.ts index d23780c..1930d17 100644 --- a/src/schemas/orders.schema.ts +++ b/src/schemas/orders.schema.ts @@ -14,7 +14,7 @@ const OrderItemSchema = z.object({ .transform((v) => Number(v) || 0) .optional() .default(0), - is_included_in_total: z.any().optional().default(true), + is_included_in_total: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(true), position: z .union([z.number(), z.string()]) .transform((v) => Number(v)) @@ -55,7 +55,7 @@ export const CreateOrderSchema = z.object({ .transform((v) => Number(v)) .optional() .default(21.0), - apply_vat: z.any().optional().default(true), + apply_vat: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(true), exchange_rate: z .union([z.number(), z.string()]) .transform((v) => Number(v)) @@ -82,7 +82,7 @@ export const UpdateOrderSchema = z.object({ .union([z.number(), z.string()]) .transform((v) => Number(v)) .optional(), - apply_vat: z.any().optional(), + apply_vat: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), items: z.array(OrderItemSchema).optional(), sections: z.array(OrderSectionSchema).optional(), }); diff --git a/src/schemas/trips.schema.ts b/src/schemas/trips.schema.ts index 9ed935a..a079635 100644 --- a/src/schemas/trips.schema.ts +++ b/src/schemas/trips.schema.ts @@ -11,7 +11,7 @@ export const CreateTripSchema = z.object({ end_km: z.union([z.number(), z.string()]).transform((v) => Number(v)), route_from: z.string(), route_to: z.string(), - is_business: z.any().optional().default(false), + is_business: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(false), notes: z.string().nullish(), }); @@ -27,7 +27,7 @@ export const UpdateTripSchema = z.object({ .optional(), route_from: z.string().optional(), route_to: z.string().optional(), - is_business: z.any().optional(), + is_business: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), notes: z.string().nullish(), }); diff --git a/src/schemas/users.schema.ts b/src/schemas/users.schema.ts index 092bf0e..fa38f91 100644 --- a/src/schemas/users.schema.ts +++ b/src/schemas/users.schema.ts @@ -7,7 +7,7 @@ export const CreateUserSchema = z.object({ first_name: z.string().min(1, "Jméno je povinné"), last_name: z.string().min(1, "Příjmení je povinné"), role_id: z.union([z.number(), z.string()]).transform((v) => Number(v)), - is_active: z.any().optional().default(true), + is_active: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(true), }); export const UpdateUserSchema = z.object({ @@ -17,7 +17,7 @@ export const UpdateUserSchema = z.object({ first_name: z.string().optional(), last_name: z.string().optional(), role_id: z.union([z.number(), z.string(), z.null()]).optional(), - is_active: z.any().optional(), + is_active: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), }); export type CreateUserInput = z.infer; diff --git a/src/schemas/vehicles.schema.ts b/src/schemas/vehicles.schema.ts index 9877265..729a621 100644 --- a/src/schemas/vehicles.schema.ts +++ b/src/schemas/vehicles.schema.ts @@ -15,7 +15,7 @@ export const CreateVehicleSchema = z.object({ .transform((v) => Number(v)) .optional() .default(0), - is_active: z.any().optional().default(true), + is_active: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional().default(true), }); export const UpdateVehicleSchema = z.object({ @@ -31,7 +31,7 @@ export const UpdateVehicleSchema = z.object({ .union([z.number(), z.string()]) .transform((v) => Number(v)) .optional(), - is_active: z.any().optional(), + is_active: z.preprocess(v => v === true || v === 1 || v === "1", z.boolean()).optional(), }); export type CreateVehicleInput = z.infer;