mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-27 10:46:51 +00:00
fix(audit-log): neutralize CSV export injection, clamp pagination, bound anomaly history (#1259)
* fix(audit-log): neutralize CSV export injection, clamp pagination, bound anomaly history Harden the Admiral audit log without changing its tier or hub-only gating. - CSV export now defuses formula injection: any field that a spreadsheet would evaluate as a formula (leading = + - @, or a trigger behind leading whitespace, or a leading tab/CR) is prefixed with a single quote before RFC 4180 quoting. Audit summaries embed user-controlled resource names, so this closes a path where a crafted name could execute on export open. - Clamp page and limit to positive bounds on the list endpoint so a negative limit can no longer reach SQLite as "unlimited" and dump the whole table. - Bound the anomaly and stats history reads to a capped slice of recent rows so the analysis paths stay within fixed memory and latency on large histories instead of scanning the full retention window per request. - Surface failed audit list and stats fetches through the standard error toast instead of leaving the view silently stale. - Add a developer-mode-gated diagnostic log to the stats endpoint for parity with the list and export handlers. Covered by new unit and HTTP-integration tests (CSV neutralization through the real export route, pagination clamps, bounded history, stats endpoint, anomaly annotation) and verified end to end in the browser. * fix(audit-log): compute signal-rail stats with exact SQL aggregates Address review feedback on the earlier history-cap change. The cap was correct for the anomaly baseline but made the stats tiles (events, actors, failure rate, hourly series) silently undercount on a hub with more than the cap's worth of rows in the window, since they were derived from the capped row slice. - Add DatabaseService.getAuditStatsInputs: exact counts via SQL COUNT / COUNT(DISTINCT) / GROUP BY hour, and new-ip detection over the small DISTINCT (user, ip) pair sets. No row cap, so the tiles stay exact at any window size while memory stays bounded. - Reduce computeAuditStats to a pure formatter over those aggregates. - Keep the bounded history read only for the list endpoint's anomaly annotation, where a recent-activity baseline is an acceptable heuristic. - Skip the redundant load-failure toast when a fetch fails with a handled 401, so an expired session does not stack toasts on top of logout. Adds exactness tests for the aggregate counts, distinct-actor handling, and new-ip detection, and strengthens the pagination-clamp tests. * fix(audit-log): exclude future-dated rows and make the new-ip sample deterministic Two small parity fixes on the stats aggregates: upper-bound every current window by `now` so a future-dated row (clock skew or a fixture) cannot inflate the live counts, and order the new-ip pair scan so the sample actor shown in the tile detail is stable. Adds a test asserting a future row is excluded.
This commit is contained in:
@@ -1,12 +1,29 @@
|
||||
// Matches a value a spreadsheet would evaluate as a formula: a metacharacter
|
||||
// (= + - @), optionally preceded by whitespace, since Excel / Sheets /
|
||||
// LibreOffice ignore leading blanks when detecting formulas. A leading tab or
|
||||
// carriage return is also treated as a trigger because some parsers strip it
|
||||
// before evaluating what follows. Audit summaries embed user-controlled
|
||||
// resource names, so any field can reach the export untrusted.
|
||||
const FORMULA_LEAD_RE = /^\s*[=+\-@]/;
|
||||
|
||||
function startsFormula(str: string): boolean {
|
||||
return str[0] === '\t' || str[0] === '\r' || FORMULA_LEAD_RE.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a single field for RFC 4180 CSV output. Wraps the value in quotes
|
||||
* and doubles embedded quotes when the field contains a comma, quote, or
|
||||
* newline. Null / undefined become the empty string.
|
||||
* Escape a single field for RFC 4180 CSV output. Neutralizes formula injection
|
||||
* by prefixing a single quote when the value would be read as a formula
|
||||
* (CWE-1236), then wraps the value in quotes and doubles embedded quotes when
|
||||
* it contains a comma, quote, or line break. Null / undefined become the empty
|
||||
* string.
|
||||
*/
|
||||
export function escapeCsvField(val: string | number | null | undefined): string {
|
||||
if (val === null || val === undefined) return '';
|
||||
const str = String(val);
|
||||
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
||||
let str = String(val);
|
||||
if (str.length > 0 && startsFormula(str)) {
|
||||
str = `'${str}`;
|
||||
}
|
||||
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
||||
return `"${str.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return str;
|
||||
|
||||
Reference in New Issue
Block a user