Commit e0dfd0e
authored
* Fix sbpp#1335 C1 + M1: harden panel-runtime guard, swap bare die() for chrome
C1 (critical, panel-takeover via localhost host-header bypass): pre-fix
`web/init.php`'s install/ + updater/-presence guard was gated on
`$_SERVER['HTTP_HOST'] != "localhost"`, so any panel reachable via a
`localhost` Host header (port-forward, SSH tunnel, ngrok, Cloudflare
Tunnel) silently bypassed the safety check. Combined with the absent
wizard-side gate (sbpp#1335 C2, separate commit), an attacker could re-run
the wizard over a live install and overwrite `config.php` / admin / DB.
The exemption is gone; the guard is now unconditional in production,
with a single explicit `SBPP_DEV_KEEP_INSTALL` constant as the dev
escape hatch (loud-named so a production-side define is visibly wrong;
defined automatically for the docker dev stack via
`docker/php/dev-prepend.php`'s `auto_prepend_file` hook). The legacy
`IS_UPDATE` exemption — used by the updater itself — is preserved.
M1 (major, error chrome): the wizard's done-page CTA sends operators
straight to `/`. Pre-fix, that landed on bare-text
`die('SourceBans++ is not installed')` /
`die('Please delete the install directory')` /
`die('Composer autoload not found')` — stark white 200-response with no
chrome / no link to docs / no fix instructions. Reads like a server
crash to a non-technical self-hoster. Replaced with self-contained
inline-HTML error pages in `web/init-recovery.php`'s
`sbpp_render_install_blocked_page()` (mirror of `recovery.php`'s
contract: no Composer / no Smarty / no `Sbpp\…`, since the surface runs
upstream of the autoload chain). Bonus per the issue: missing-`config.php`
now redirects to `/install/` instead of dying so a fresh-tarball
operator lands directly in the wizard.
Guard logic and rendering live in pure functions
(`sbpp_check_install_guard()` + `sbpp_render_install_blocked_page()`) so
the contract is unit-testable in isolation.
`web/tests/integration/InstallGuardTest.php` pins:
- localhost Host header does NOT bypass the guard (C1 regression)
- IS_UPDATE skips the guard (preserved legacy contract)
- SBPP_DEV_KEEP_INSTALL skips the guard (new dev escape hatch)
- the C2 already-installed predicate (sister-guard, separate commit)
- the m4 PDO error translation (separate commit)
The dev stack:
- `docker/php/dev-prepend.php` defines `SBPP_DEV_KEEP_INSTALL` (replaces
the pre-sbpp#1335 `HTTP_HOST` rewrite trick — that whole shape was the
vulnerability path)
- `docker/Dockerfile` and `docker-compose.yml` comment-only updates
document the new mechanism
* Fix sbpp#1335 C2: refuse wizard start over already-installed panel
Pre-fix, `web/install/index.php` had no "is the panel already
installed?" gate. Anyone reaching `/install/` after a successful wizard
run (operator forgot to delete `install/`, or guard bypass via sbpp#1335
C1's localhost host-header trick) could walk the wizard end-to-end
again, overwriting `config.php` (when writable), creating a new admin
account, and re-pointing the panel at a different DB — a complete
panel-takeover path.
The wizard now refuses to start when `config.php` exists in the panel
root, surfacing `web/install/already-installed.php` (pure inline HTML +
CSS, mirror of `recovery.php`'s contract: no Composer / no Smarty / no
`Sbpp\…` since the gate runs BEFORE `bootstrap.php`'s autoload pull).
The page emits 409 Conflict, links the operator back to `/`
(already-installed panels boot from there), and explains the
intentional-reinstall path: delete `config.php` first. No
confirm-dialog bypass is offered — the explicit delete step forces the
operator to acknowledge the impact before the wizard touches any state.
The guard predicate is a pure function
(`sbpp_install_is_already_installed()`) so the contract is unit-testable
without a runtime install; the regression test lives in
`web/tests/integration/InstallGuardTest.php` (added in the previous
commit alongside the C1 / M1 / m4 coverage).
Sister-guard to the runtime-side `sbpp_check_install_guard()` from the
previous commit; both key off `config.php` so the contract is symmetric.
* Fix sbpp#1335 M2 + M3 + m1-m6: install wizard human-flow polish
Wizard-side UX fixes from the sbpp#1335 audit, grouped into one commit
because every change is a localized template / handler tweak that
compounds the same goal: stop the wizard from feeling broken when
something goes wrong on the operator's first touch.
M2 (writable folder fail message): page 3 surfaced "Not writable:
/path/..." with no remediation. Now extends with a one-liner pointing
operators at chmod 0775 (or 0777 on shared hosting where the PHP user
isn't yours) via File Manager / FTP / chmod. Plain text, not HTML —
the surrounding Smarty template auto-escapes.
M3 (admin step round-trip): step 5 wipes both password fields on every
validation re-render (correct for `nofilter` avoidance, wrong for UX).
Extended the existing page-tail vanilla-JS guard (already covered the
mismatched-password case) with SteamID format + email shape checks so
the round-trip-with-wiped-passwords path stops being the common case.
Server-side validation stays as the second line of defense.
m1 (recovery.php direct hit): pre-fix the surface always emitted 503,
even when `vendor/autoload.php` was actually present (someone
bookmarked the URL, an operator visited it directly out of curiosity).
Self-checks vendor presence at the top and 302s to `/install/` if
present.
m2 (license/licence consistency): standardized on American "License"
across step 1 (page handler + template) — matches LICENSE.md, the
testid prefix `install-license-*`, and the rest of the repo's
spelling.
m3 (testid prefix sweep): step 2 used mixed `install-db-*` (fields)
and `install-database-*` (form / buttons). Standardized field testids
on `install-database-*` to match every other step's pattern.
m4 (PDO error translation): step 2 surfaced raw PDOException strings
("SQLSTATE[HY000] [1045] Access denied for user 'sourcebans'@'192.168.96.5'
(using password: YES)") — gibberish to non-DBAs plus the panel-as-seen-
by-DB internal IP is a minor information disclosure. New
`sbpp_install_translate_pdo_error()` helper in
`web/install/includes/helpers.php` pattern-matches the four codes a
non-technical operator is most likely to hit (1045 access denied / 2002
host unreachable / 1049 unknown database / 1044 denied for user on
database) and emits a friendly translation; falls back to the raw
message for unrecognised codes so debugging stays possible. Regression
test in `web/tests/integration/InstallGuardTest.php` (added with C1).
m5 (license textarea height): step 1's `<textarea ... rows="20">`
was overridden by `.input { height }` from the wizard's CSS, collapsing
to one row. Switched to the panel's `.textarea` class with an inline
`min-height: 24rem` so the licence is readable without scroll-noise.
m6 (AMXBans step helper text): step 6's labels-only fields gave
operators no hint where to find the values. Added a top-of-form
"Look in `addons/amxmodx/configs/sql.cfg`" pointer plus per-field
helper text mirroring `page_database.tpl`'s shape.
* docs: AGENTS.md + ARCHITECTURE.md + README.md updates for sbpp#1335
Doc-and-code drift is a defect per AGENTS.md's "Keep the docs in sync"
rule. Update both panels + the user-facing README to reflect the sbpp#1335
fixes:
AGENTS.md:
- Install wizard lifecycle: insert the C2 already-installed gate
between paths-bootstrap and recovery short-circuit; document the
sister-guards on either side of the wizard (panel-runtime
`init-recovery.php` + wizard-side `already-installed.php`) and
the new `SBPP_DEV_KEEP_INSTALL` dev-only opt-in
- "Where to find what": three new rows for the friendly-error
surface, the wizard's already-installed gate, and the PDO error
translator
- "Edit a step of the install wizard" row: refreshed to mention
the new helper functions, the licence→license sweep (m2), the
testid standardization (m3), the page-tail JS validation
additions on step 5 (M3), and the textarea height fix (m5).
- Anti-patterns: four new entries — `HTTP_HOST` magic on the
guard, allowing the wizard to start over an installed panel,
bare-text `die()` in `init.php`, and surfacing raw `PDOException`
strings to operator-facing banners.
- Wizard-vanilla-JS anti-pattern updated to mention the M3 step-5
admin-form validation extensions and standardize on "license"
spelling.
ARCHITECTURE.md:
- Web panel directory layout: surface `init-recovery.php` and
`already-installed.php` and their sbpp#1335 IDs in the tree.
- Bootstrap step 2: replace the localhost-host-exemption sentence
with the C1 + M1 + dev-escape-hatch description.
- Local dev stack: rewrite the `dev-prepend.php` paragraph (used
to be `HTTP_HOST` rewrite, now `SBPP_DEV_KEEP_INSTALL` define).
- Legacy patterns table: three new rows for the C1, M1, and C2
pre-fix shapes.
README.md (m7):
- New paragraph between install steps 3 and 4 telling operators
how to set folder permissions if the wizard's environment check
complains. Matches the M2 in-product remediation hint.
* Fix sbpp#1335 M2 review: split missing-folder hint from not-writable hint
PR sbpp#1336's first cut of M2 appended the same chmod-flavored hint
("set permissions to 0775 ... via your hosting File Manager, FTP
client, or chmod") to BOTH the `Missing:` AND `Not writable:`
branches in `web/install/pages/page.3.php`. For a missing directory
the operator can't chmod something that doesn't exist — they need
to re-upload from the release zip (or `mkdir`). The release tarball
ships a placeholder for every required folder (`web/demos/.gitkeep`,
`web/cache/`, the bundled `web/images/games/*.png` and
`web/images/maps/*` files), so a `Missing:` status indicates a
partial / broken upload, not a permission problem.
Lift the detail-string construction into
`sbpp_install_describe_filesystem_check()` in
`web/install/includes/helpers.php` — pure function over
`(path, exists, writable)` returning the `{$row.detail}` text the
template renders. Three branches, three distinct remediations:
- Missing → "re-upload this folder from the release zip, or
create it via your hosting File Manager."
- Not writable → the existing chmod 0775 hint (paired with
README m7's signpost so the two surfaces stay in sync).
- OK → bare 'Writable' (no hint needed; the row already shows
a green check).
The pure-function shape is unit-testable. New regression test
`testFilesystemCheckEmitsDistinctRemediations` in
`web/tests/integration/InstallGuardTest.php` pins the contract
in both directions (the missing branch must NOT mention chmod;
the not-writable branch must NOT suggest re-uploading) so a
future drift can't silently re-merge the two hints.
* Fix sbpp#1335 M3 review: drop novalidate, native attrs gate every input case
PR sbpp#1336's first cut of M3 added SteamID + email + password-match
checks to the admin-form's page-tail JS — but the form carried
`novalidate`, which switched off the browser's pre-submit checks
for `required` / `minlength="8"` / `pattern` / `type="email"`.
Two follow-up gaps:
- **Short password** (`<8` chars): native `minlength` was off,
JS only checked match — a 5-char password matching its 5-char
confirm passed the JS guard, then bounced server-side
("Password must be at least 8 characters.") wiping both
fields.
- **Empty fields**: native `required` was off, JS didn't
explicitly check emptiness — every empty username / email /
SteamID hit the server's `'All fields are required.'` and
wiped both passwords on the re-render.
Both gaps are symptomatic of the same root cause: `novalidate`
violates AGENTS.md's install-wizard rule that "the form's native
`required` / `pattern` attributes must be the load-bearing gate,
with JS as the UX polish". The fix drops `novalidate` and lets
the native attrs cover empty / short / pattern / type cases the
way the rule intends — the browser surfaces its popover before
our submit handler runs.
The JS handler shrinks to just the cross-field password-match
check (the one validation native HTML can't express). On submit,
native runs first; if everything natively-valid, our handler
runs and `setCustomValidity('Passwords do not match.')` +
`reportValidity()` + `e.preventDefault()` re-uses the same
native popover surface for the custom message.
`autocomplete="new-password"` + the never-echo-back-into-template
contract on the password fields are unchanged; this is purely
about which validations gate submission and how the operator
sees the failure (native popover anchored to the field vs
server-side bounce that wipes passwords).
* docs: AGENTS.md install-wizard cross-field-validation contract
Two paired updates riding the M3 review fix in the previous commit:
1. The "Conventions for new wizard work" block's "Forms POST
natively" rule grows a `novalidate` carve-out spelling out
the canonical "cross-field validation" shape. The existing rule
was correct ("the form's native `required` / `pattern`
attributes must be the load-bearing gate, with JS as the UX
polish"), but didn't explicitly call out `novalidate` as the
anti-pattern that defeats it. Future wizard steps with a
genuine cross-field need (cf. step 5's password-match) follow
the canonical shape: keep native validation on, hook submit,
set customValidity in the handler, clear it on input.
2. The wizard-vanilla-JS anti-pattern entry (in "Anti-patterns",
under the install-wizard heading) updates from "the admin
form's password-match + SteamID + email shape checks" to
"the admin form's cross-field password-match check" — the
SteamID + email checks that PR sbpp#1336 first added are now
handled natively by the form's `pattern` and `type="email"`
attrs, so listing them as JS-territory was already stale.
3. The "Edit a step of the install wizard" row in "Where to find
what" gets `sbpp_install_describe_filesystem_check` added to
the helper list (paired with the M2 review fix's lift of the
filesystem-check string-building into a pure helper).
1 parent f0571f8 commit e0dfd0e
20 files changed
Lines changed: 1511 additions & 103 deletions
File tree
- docker
- php
- web
- install
- includes
- pages
- tests/integration
- themes/default/install
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
114 | 114 | | |
115 | 115 | | |
116 | 116 | | |
| 117 | + | |
117 | 118 | | |
118 | | - | |
| 119 | + | |
119 | 120 | | |
120 | 121 | | |
121 | 122 | | |
122 | | - | |
| 123 | + | |
| 124 | + | |
123 | 125 | | |
| 126 | + | |
124 | 127 | | |
125 | 128 | | |
126 | 129 | | |
| |||
147 | 150 | | |
148 | 151 | | |
149 | 152 | | |
150 | | - | |
151 | | - | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
152 | 162 | | |
153 | 163 | | |
154 | 164 | | |
| |||
913 | 923 | | |
914 | 924 | | |
915 | 925 | | |
916 | | - | |
917 | | - | |
918 | | - | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
919 | 935 | | |
920 | 936 | | |
921 | 937 | | |
922 | 938 | | |
923 | 939 | | |
924 | 940 | | |
925 | | - | |
926 | | - | |
| 941 | + | |
| 942 | + | |
927 | 943 | | |
928 | 944 | | |
929 | 945 | | |
| |||
1073 | 1089 | | |
1074 | 1090 | | |
1075 | 1091 | | |
| 1092 | + | |
| 1093 | + | |
| 1094 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
57 | 64 | | |
58 | 65 | | |
59 | 66 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
8 | | - | |
9 | | - | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
41 | 41 | | |
42 | 42 | | |
43 | 43 | | |
44 | | - | |
45 | | - | |
46 | | - | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
4 | | - | |
5 | | - | |
6 | | - | |
7 | | - | |
8 | | - | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
14 | 30 | | |
0 commit comments