-
Notifications
You must be signed in to change notification settings - Fork 0
SECURITY
Please report suspected security issues privately rather than opening a public issue.
Open a GitHub Security Advisory on this repository, or email the maintainers listed in
composer.json / package.json. We aim to acknowledge reports within a few business
days. This is a young project without a formal bug-bounty program — please don't expect
one, but genuine reports are taken seriously and credited.
No software honestly earns a "zero bugs, fully audited" claim on day one — that phrase gets used a lot in marketing and rarely means anything. What this project does instead, per ARCHITECTURE.md §6, is commit to a process, and this document is the running record of that process actually being followed — including the bugs it already found.
-
No
eval()/new Function()anywhere in the formula engine (TS or PHP). The engine is a real tokenizer → parser → AST → evaluator specifically so that formula text is never handed to a language interpreter. This is the single highest-value security decision in the codebase — spreadsheet formula engines that shell out toevalare a recurring, well-known vulnerability class. -
Text-node-only DOM rendering —
LombokTable/LombokSheetnever useinnerHTMLfor cell content, so a cell value like<img src=x onerror=...>renders as inert text, not markup. Covered bytests/dom.test.ts. -
Resource-exhaustion guards on every import path:
- CSV/JSON/HTML decoders (TS and PHP) reject oversized input via a configurable
maxInputBytes(default 100MB), and CSV additionally caps row count (maxRows, default 2,000,000). - The hand-written ZIP reader (used by the XLSX codec) enforces
maxEntrySize,maxTotalSize, andmaxEntries— and critically, the per-entry limit is enforced by Node'szlibviamaxOutputLengthagainst the actual decompressed bytes, not just the (attacker-controlled) declared size in the zip header. A header can lie;zlibcan't be lied to the same way. Covered bytests/security.test.ts. - The formula parser (TS and PHP) enforces a maximum AST nesting depth (200) against both deeply parenthesized expressions and long unary-operator chains, to prevent a stack-overflow denial-of-service from a single malicious formula string. Covered in both languages' security test suites.
- CSV/JSON/HTML decoders (TS and PHP) reject oversized input via a configurable
-
No backtracking regexes on untrusted input. The HTML decoder and the formula
lexer both scan by index rather than with patterns that can backtrack. This is a
correction, not an original design decision: CodeQL flagged three genuine
polynomial-ReDoS sites (
js/polynomial-redos), and the measurements confirmed them — the oldident.replace(/[0-9]+$/, '')in the lexer took 100ms / 385ms / 1559ms on 10k / 20k / 40k-character identifiers, quadrupling on each doubling. The replacements run the same inputs in 3ms / 5ms / 10ms. Timed regression tests intests/security.test.tsfail if the backtracking forms return. -
Circular formula references resolve to a
#CIRC!error value instead of infinite-looping or crashing the process (avisiting-set check per evaluation chain). Covered bytests/formula.test.ts. -
Fuzz/property testing on the undo/redo transaction layer:
tests/fuzz.test.tsruns 200 seeded-random sequences of edits/undo/redo (deterministic seeds, so any failure is reproducible) and asserts core invariants never break. This test already found and led to fixing a real bug — see below. -
Dependency audit:
npm auditreports zero known vulnerabilities across the full dependency tree as of this writing (see CI, which runsnpm audit --audit-level=highon every push). The PHP port currently has zero runtime dependencies (only a dev-dependency on PHPUnit), so there is no dependency surface to audit there yet. -
Strict typing:
tsc --strict(includingnoUncheckedIndexedAccess) on the TS side;declare(strict_types=1)throughout the PHP port; Go's static type system plusgo vetclean on the Go port. -
Go-specific:
Evaluate()wraps AST evaluation in arecover(), so an unexpected internal failure degrades to a#ERROR!value instead of crashing the process — Go's idiomatic equivalent of the "never throw on data" contract the TS/PHP engines already follow. The Go port also uses Go's standard-libraryencoding/csvandencoding/jsonrather than hand-rolled parsers, since those aren't third-party dependencies the way an external package would be (contrast with the TS core's XLSX codec, which hand-rolls specifically to avoid a third-party dependency).
-
Undo did not restore sheet dimensions. The fuzz test in
tests/fuzz.test.tsfound thatTransactionalSheet.undo()correctly reverted cell values but leftrowCount/colCountunchanged, so undoing an edit that had grown the sheet left stale, oversized dimensions. Worse, this interacted with a second bug —Sheet.toRows()didn't bounds-check cell references against currentrowCount/colCount, so stale cells beyond a (hypothetically) shrunk boundary could leak back in as jagged, uneven rows. Both are fixed: edits now snapshot before/after dimensions, undo/redo restore them exactly via a newSheet.resize(), andtoRows()bounds-checks every cell reference. Regression tests for both are intests/fuzz.test.tsandtests/model.test.ts. The Go port (built after this was found) includes the equivalent bounds-check inToRows()from the start, with its own regression test (TestResizeShrinksDimensionsAndToRowsRespectsIt) — proof that documenting a found bug actually prevented it from being reintroduced in the next port, which is the entire point of writing it down here instead of just fixing it quietly.
This section will keep growing as more auditing happens — that's the intent. A security document with zero findings on a project handling formula parsing, file format decoding, and third-party file import isn't a sign of a clean codebase, it's a sign no one looked hard enough yet.
Covered by the practices above: the TS/JS core (src/), the PHP port
(ports/php/), and the Go port (ports/go/) — all three have the resource-exhaustion
guards and formula-parser depth guard described above, and no dynamic code execution
anywhere. Not yet covered (because it doesn't exist yet): the Rust port. When it
lands, it inherits this same checklist before being marked "done" in ARCHITECTURE.md's
roadmap — see ARCHITECTURE.md §8 for the cross-language parity standard it's held to.
If you find something not covered above, please report it — that's exactly what this process is for.