# Security > Status: canonical. Updated 2026-09-04. > Full audit: [`../ROBUSTNESS_REVIEW.md`](../ROBUSTNESS_REVIEW.md) > (Phase 0-5, 43 findings, 61 Python + 54 PHP regression tests). ## Overview bbsengine6's security posture is documented in two layers: 1. **The Phase 0-5 hardening audit** ([`../ROBUSTNESS_REVIEW.md`](../ROBUSTNESS_REVIEW.md)) — 43 findings across Python runtime, Python security, the PHP web layer, and the Python I/O / UI layer. Every finding is paired with the file/line that triggered it, the fix that was applied, and a regression test. This is the authoritative record. 2. **This document** — a high-level summary, plus the security guarantees of the canonical subsystems (session, CSRF, ident auth, router). If a security detail is in dispute, the audit wins. ## Phase 2 — Python security (summary) The Python half of the audit (Findings 2.1 – 2.9) covered nine classes of issue. Highlights: - **Password hashing migrated from SHA-256 to scrypt** (Finding 2.1). SHA-256 with no salt was brute-forceable in seconds on commodity hardware. The new format is `$scrypt$n=,r=,p=

$$` with `BBSENGINE_SCRYPT_N/_R/_P` defaults (`n=2**14, r=8, p=1`) chosen to stay under the sandbox's 64 MiB envelope. Verification uses `hmac.compare_digest`. Legacy SHA-256 hashes are still recognised on read. 10 regression tests in `py/tests/test_password_hash_scrypt.py`. - **`module.runcallback()` no longer calls `eval()`** (Finding 2.2). The dotted-callback path used `eval(f"{modpath}.{fname}")` — replaced with explicit `importlib.import_module(...)` + `getattr(...)`. A hostile configuration file could no longer execute arbitrary Python. - **Bank transfers wrapped in `SELECT ... FOR UPDATE`** (Finding 2.3). The old code did a balance read in Python, computed the new balance, and wrote it back — a TOCTOU race that let two concurrent transfers double-spend. The fix performs the increment inside the locked statement (`balance = balance + ?`). - **`util.get_safe_path` rewrites** (Finding 2.9). The old `os.path.commonpath` containment check could be bypassed when the base was a relative path (sibling-prefix collision). The fix resolves both sides with `os.path.abspath(...)` and asserts the relative path doesn't start with `..`. 9 regression tests. - **`Packet.decode` bounds check** (Finding 2.7). A peer could send `payload_len = 0xFFFFFFFF` (4 GB) and the decoder would either allocate gigabytes or raise `OverflowError`. Now rejects payloads larger than `MAX_PAYLOAD_SIZE` and truncated payloads. - **`blurb.py` path containment + `psycopg3` migration** (Finding 2.6). Path is checked with `pathlib.Path.resolve()` + `Path.is_relative_to()`. Approval now persists to the DB (`UPDATE engine.__blurb SET approved = TRUE`). - **`pgrole.py` uses `psycopg.sql.Identifier`** (Finding 2.5). Schema and table names are no longer interpolated raw. - **`verifyMemberFound` whitelist** (Finding 2.4). The column name was interpolated raw into `WHERE = ?`; now restricted to `moniker`, `email`, `loginid`. - **`folder.py` returns DB connection to the pool** (Finding 2.8). The full Python side is in [`../ROBUSTNESS_REVIEW.md`](../ROBUSTNESS_REVIEW.md#phase-2--python-security-hardening). ## Phase 3 — PHP web layer (summary) The PHP half of the audit (Findings 3.1 – 3.14) covered fourteen classes of issue. Highlights: - **`redact_secrets()` everywhere** (Finding 3.1). `engine.php` had `var_export($args, true)` in traceback handlers, dumping plaintext passwords from `$_POST["password"]` into logs and HTTP responses. The new helper walks arrays and replaces any key matching `/password|passwd|repeat|secret|token|api[_-]?key|credential|hash/i` with `"***"`. All `var_export` / `echo_traceback` calls now go through it. 11 regression tests. - **No raw `PDOException` to clients** (Finding 3.2). `echo_traceback` logs the full exception to syslog and emits a generic error to the client — SQLSTATE codes and raw messages are scrubbed. - **Session cookie hardening** (Findings 3.3 – 3.6): - `lifetime = 0` was deleted from the cookie on every request (literal `0` made the browser expire it immediately). Now `time() + $expire`. - `Secure` auto-derived from `$_SERVER['HTTPS']` and `HTTP_X_FORWARDED_PROTO` (works behind reverse proxies). - `HttpOnly = true`, `SameSite = Lax`. - Session id is rotated after validation; the new id is sent back via `setcookie()` (otherwise the rotation was lost on the next request). - Session id format is checked against `^[A-Za-z0-9,\-]{1,128}$` before DB lookup. - **`autoExecute` safe WHERE clause** (Finding 3.8). Old signature accepted a raw `$where` string concatenated into the SQL. New signature: `autoExecute($dbh, $table, $data, $mode, ?string $where = null, array $whereParams = [])`. Empty `$where` is rejected for UPDATE/DELETE; callers are forced to bind values. - **`libmember.checkflag` scalar fix** (Finding 3.7). Old code subscripted `$stmt->fetchColumn()` like an array and always returned null — every flag check silently failed. Now fetches the scalar. - **`accesspost("add", ...)` auth check** (Finding 3.9). Unauthed callers could insert rows. Now requires a matching member id and feature flag. - **Parsedown safe mode** (Finding 3.10). `engine/router.php` was rendering user Markdown with raw HTML enabled (`