500 * 1024 * 1024) { return false; } // Allow-list check against the FINAL extension only, case-insensitive $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if ($extension === '' || !in_array($extension, $allowed_extensions, true)) { return false; } // Unguessable storage name. We deliberately do NOT hash file contents: // randomString(32) already guarantees uniqueness, and hashing would mean // reading the whole file into memory (up to 500 MB) for no downstream use. return randomString(32) . '.' . $extension; } // Neutralize spreadsheet formula injection (CWE-1236) in a value bound for a // generated CSV export. This is NOT CSV-structure escaping — fputcsv already // quotes fields, doubles enclosures, and handles embedded newlines. Its only // job is to stop a spreadsheet app (Excel / LibreOffice Calc / Google Sheets) // from *evaluating* a syntactically valid cell as a formula on open. Prefixing // a single quote makes the app treat the cell as literal text. function escapeCsvFormula($value) { if (!is_string($value) || $value === '') { return $value; } // Leave genuine numbers untouched so real data (e.g. negative amounts like // -42.50) isn't corrupted. An actual formula string is never is_numeric(), // so nothing dangerous slips through this early return. if (is_numeric($value)) { return $value; } // Leading characters that trigger formula / legacy-DDE evaluation on open. if (in_array($value[0], ['=', '+', '-', '@', "\t", "\r"], true)) { return "'" . $value; } return $value; } /** * Render a flash alert message for output. * * flashAlert() messages deliberately carry a little formatting markup - roughly * 480 of the ~653 call sites wrap a value in - while 429 of them also * interpolate PHP variables that are user controlled (custom link names, ticket * status names, saved payment descriptions and so on). Escaping the lot would * print the tags; printing the lot raw is an XSS hole. * * So: escape everything, then restore a fixed allowlist of formatting tags that * carry no attributes and cannot execute. Attribute injection, , event * handlers and are all neutralised because the escaped forms are never * put back. * * Every place that displays $_SESSION['alert_message'] must use this. Before it * existed there were four separate implementations and one of them interpolated * raw. */ /** * Map a flashAlert type onto a real Bootstrap contextual class. * * flashAlert() is called with more type names than Bootstrap has classes - * 'error', 'alert' and a typo'd 'errpr' among them - so interpolating the raw * value produced alert-error / alert-alert, which style nothing at all. */ function alertStyleClass($type) { $alert_classes = [ 'success' => 'success', 'info' => 'info', 'warning' => 'warning', 'alert' => 'warning', 'error' => 'danger', 'errpr' => 'danger', 'danger' => 'danger', ]; return $alert_classes[$type] ?? 'secondary'; } function alertMessageHtml($message) { $allowed_tags = [ '', '', '', '', '', '', '', '', '', '', '
', '
', '
', ]; $safe = htmlspecialchars((string) $message, ENT_QUOTES, 'UTF-8'); foreach ($allowed_tags as $tag) { $safe = str_replace(htmlspecialchars($tag, ENT_QUOTES, 'UTF-8'), $tag, $safe); } return $safe; }