Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ jobs:
context: .
file: docker/Dockerfile.prod
platforms: linux/amd64,linux/arm64
# Always re-resolve the floating `php:8.5-*` base tags so a
# PHP security release lands on the next publish instead of
# being masked by a cached base layer.
pull: true
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5051,6 +5051,6 @@ the spec, target a 1920px viewport, not 1440px.
| Translate raw `PDOException` connect errors into operator-friendly messages on the wizard's database step | `sbpp_install_translate_pdo_error()` in `web/install/includes/helpers.php` (#1335 m4). Pattern-matches the four error 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 friendlier translation; falls back to the raw message for unrecognised codes so debugging stays possible. Pre-fix the wizard surfaced `SQLSTATE[HY000] [1045] Access denied for user 'sourcebans'@'192.168.96.5' (using password: YES)` verbatim, which is gibberish to non-DBAs and includes the panel-as-seen-by-DB internal IP (minor information disclosure). Regression test: `web/tests/integration/InstallGuardTest.php::testPdoErrorTranslationCoversCommonCodes`. |
| Run a stack in parallel with another worktree | Worktree-local `docker-compose.override.yml` (see "Parallel stacks") |
| Local dev stack details | `docker/README.md` |
| Build / extend the production Docker image (multi-stage build, hardened runtime, entrypoint state machine, healthcheck) | `docker/Dockerfile.prod` (multi-stage: `builder` runs `composer install --no-dev` against `web/`; `runtime` carries pdo_mysql + intl + zip + mbstring + gmp ONLY — no nodejs / npm / git / dev-prepend) + `docker/php/prod-entrypoint.sh` (pure POSIX shell state machine: `*_FILE` secret resolution → DATABASE_URL parse → defaults → Apache config (PORT + mod_remoteip from `SBPP_TRUSTED_PROXIES`) → wait-for-DB → render config.php (only when missing) → first-boot install (schema + data + seed admin from `INITIAL_ADMIN_*` env vars) → headless updater migrations → strip install/ + updater/ from writable layer → ensure writable cache/templates_c/demos → `exec apache2-foreground`) + `docker/php/prod-php.ini` (production OPcache: `validate_timestamps=0`, `display_errors=Off`, `log_errors=On`, errors → `/dev/stderr`, UTC default, `expose_php=Off`) + `docker/apache/sbpp-prod.conf` (denies dotfiles + vendor/ + configs/ + includes/ + install/ + updater/ + cache/ + templates_c/ + config.php + composer.{json,lock}; `RemoteIPHeader X-Forwarded-For` for the trusted-proxy chain) + `web/health.php` (DB-aware unauthenticated healthcheck — `init.php` bootstraps the panel; `$GLOBALS['PDO']->query('SELECT 1')` returns 200 OK or 503 + plain-text reason; Cache-Control: no-store + X-Robots-Tag: noindex). The production image MUST NOT define `SBPP_DEV_KEEP_INSTALL`; the entrypoint's `strip_install_dirs` step is what makes the panel-runtime guard pass instead (#1381). |
| Build / extend the production Docker image (multi-stage build, hardened runtime, entrypoint state machine, healthcheck) | `docker/Dockerfile.prod` (multi-stage: `builder` runs `composer install --no-dev` against `web/`; `runtime` carries pdo_mysql + intl + zip + mbstring + gmp ONLY — no nodejs / npm / git / dev-prepend; a `PHP_MIN_VERSION` build-arg check fails the build when the floating `php:8.5-apache` base is below the last PHP security floor, currently 8.5.12 for CVE-2026-109618 / CVE-2026-109619 — bump it on every PHP security release, and `docker-image.yml` passes `pull: true` so a cached base can't mask the update) + `docker/php/prod-entrypoint.sh` (pure POSIX shell state machine: `*_FILE` secret resolution → DATABASE_URL parse → defaults → Apache config (PORT + mod_remoteip from `SBPP_TRUSTED_PROXIES`) → wait-for-DB → render config.php (only when missing) → first-boot install (schema + data + seed admin from `INITIAL_ADMIN_*` env vars) → headless updater migrations → strip install/ + updater/ from writable layer → ensure writable cache/templates_c/demos → `exec apache2-foreground`) + `docker/php/prod-php.ini` (production OPcache: `validate_timestamps=0`, `display_errors=Off`, `log_errors=On`, errors → `/dev/stderr`, UTC default, `expose_php=Off`, `session.upload_progress.enabled=0` + `fatal_error_backtraces=0` as CVE-2026-109619 hardening — keep these in the ini file, `ini_set()` runs too late) + `docker/apache/sbpp-prod.conf` (denies dotfiles + vendor/ + configs/ + includes/ + install/ + updater/ + cache/ + templates_c/ + config.php + composer.{json,lock}; `RemoteIPHeader X-Forwarded-For` for the trusted-proxy chain) + `web/health.php` (DB-aware unauthenticated healthcheck — `init.php` bootstraps the panel; `$GLOBALS['PDO']->query('SELECT 1')` returns 200 OK or 503 + plain-text reason; Cache-Control: no-store + X-Robots-Tag: noindex). The production image MUST NOT define `SBPP_DEV_KEEP_INSTALL`; the entrypoint's `strip_install_dirs` step is what makes the panel-runtime guard pass instead (#1381). |
| Deploy / configure the production Docker stack (compose, env vars, reverse-proxy) | `docker-compose.prod.yml` (pulls `ghcr.io/srcdslab/sourcebans-pp:${SBPP_IMAGE_TAG:-latest}` — NOT a build context; DB port NOT exposed by default; commented `caddy:` service block for opt-in TLS) + `.env.example.prod` (every supported env var grouped by required / recommended / first-boot / optional / advanced; documents the `*_FILE` Docker-secret pattern and the `SBPP_CONFIG_PATH` Docker-secret-mount pattern; uses `${VAR:?...}` compose syntax for required vars so a fresh-deploy operator who forgot `SB_SECRET_KEY` / `DB_PASS` / `DB_ROOT_PASS` gets a useful container-startup error) + `docker/caddy/Caddyfile.example` (one-line `reverse_proxy web:80` + zstd/gzip encode + static-asset cache headers). Three persistent volumes: `dbdata` + `demos` (MUST persist — DB rows + uploaded ban-evidence); `cache` + `smarty` (CAN be ephemeral — rebuild on demand). Operators run `docker compose -f docker-compose.prod.yml up -d` from a directory carrying both files; upgrades are `docker compose pull && up -d` (image is immutable, entrypoint runs idempotent migrations on every boot, named volumes survive the swap) (#1381). |
| Honour `SBPP_CONFIG_PATH` so config.php can live outside the panel root (Docker-secret mount) | `sbpp_resolve_config_path()` in `web/init-recovery.php` is the single source of truth; `web/init.php` calls it to resolve the require-site. `web/install/already-installed.php`'s `sbpp_install_is_already_installed()` re-implements the env-var read inline (per its self-contained no-Composer docblock) so the wizard-side and runtime-side guards agree on the install-state sentinel path. Pre-#1381 both halves hard-coded the panel-root path; with a Docker-secret-mounted config the runtime would 302-to-/install/ while the wizard would happily start over. Regression tests: `testResolveConfigPathHonorsEnvVar` + `testWizardGuardHonorsConfigPathEnvVar` in `web/tests/integration/InstallGuardTest.php`.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ Legend:
? = Other stuff
```

(21/10/26): Version 2.1.23-srcdslab
-----------------------
```
01. ! Security: the production Docker image refuses to build on PHP
below 8.5.12 (CVE-2026-109618, CVE-2026-109619, unauthenticated
RCE) and always pulls a fresh php:8.5 base image
02. ! Security: production php.ini disables session upload progress and
fatal error backtraces (CVE-2026-109619 hardening)
03. + Install wizard warns when PHP is older than 8.5.12
```

(20/10/26): Version 2.1.22-srcdslab
-----------------------
```
Expand Down
10 changes: 10 additions & 0 deletions docker/Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ FROM php:8.5-apache AS runtime
ENV DEBIAN_FRONTEND=noninteractive \
APACHE_DOCUMENT_ROOT=/var/www/html/web

# Refuse to build on a PHP older than the last security floor. The
# `php:8.5-apache` tag floats, so a stale local cache or a mirror that
# lags behind Docker Hub would otherwise publish a vulnerable runtime.
# 8.5.12 fixes CVE-2026-109618 + CVE-2026-109619 (unauthenticated RCE,
# worst under mod_php, which is what this image runs). Bump the floor
# on the next PHP security release.
ARG PHP_MIN_VERSION=8.5.12
RUN php -r 'exit(version_compare(PHP_VERSION, getenv("PHP_MIN_VERSION"), ">=") ? 0 : 1);' \
|| { echo "PHP $(php -r 'echo PHP_VERSION;') is below the security floor ${PHP_MIN_VERSION}; pull a newer php:8.5-apache base image." >&2; exit 1; }

# Runtime extensions only. The dev image carries `default-mysql-client`,
# `nodejs`, `npm`, `git`, `gettext-base`, `unzip` — all developer-side
# helpers the prod image has no business shipping. `pdo_mysql` is the
Expand Down
8 changes: 8 additions & 0 deletions docker/php/prod-php.ini
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ session.cookie_samesite = "Lax"
session.use_strict_mode = 1
session.gc_maxlifetime = 86400

; Hardening for CVE-2026-109619 (stale global state RCE). The panel
; never reads upload progress, and fatal-error backtraces are a
; confirmed attack path on PHP 8.5. Both must be set here, before
; request parsing; ini_set() at runtime is too late. The real fix is
; PHP >= 8.5.12, which Dockerfile.prod enforces at build time.
session.upload_progress.enabled = 0
fatal_error_backtraces = 0

; ---------------------------------------------------------------------------
; OPcache (production knobs)
; ---------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions docs/src/content/docs/getting-started/prerequisites.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ some CJK characters fail to insert and the plugin logs a SQL error.

- **PHP 8.5 or newer.** This is checked by the install wizard's
"System check" step.
- **PHP 8.5.12 or newer for production.** Older 8.5 releases have two
critical remote code execution bugs (CVE-2026-109618 and
CVE-2026-109619). The wizard warns below 8.5.12. If your distro
backported the fix, the warning is safe to ignore.
- **64 MB memory limit minimum.** The wizard doesn't enforce this,
but the panel can run out of memory on large ban-list pages below
64 MB.
Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/getting-started/quickstart-docker.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ against the existing database. The migrations are idempotent: every
script in `web/updater/data/<N>.php` is forward-only and safe to
re-run. The `dbdata` and `demos` volumes survive the swap unchanged.

PHP security fixes reach you the same way: each published image is
built on the newest `php:8.5-apache` base, and the build fails below
PHP 8.5.12 (the fix for CVE-2026-109618 and CVE-2026-109619). Pull a
fresh image after a PHP security release. Check the running version
with `docker compose -f docker-compose.prod.yml exec web php -v`.

Image tags:

| Tag | What it points at |
Expand Down
14 changes: 11 additions & 3 deletions web/install/pages/page.3.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,22 @@
// Runtime gate for self-hosters below the PHP 8.5 floor. The analyser
// environment is locked to 8.5+, so this branch is not exercised there.
$phpVersionOk = version_compare(PHP_VERSION, '8.5', '>=');
// 8.5.0-8.5.11 still run the panel but carry CVE-2026-109618 +
// CVE-2026-109619 (unauthenticated RCE). Warn rather than block: the
// operator may be on a distro build with the fix backported.
$phpPatched = version_compare(PHP_VERSION, '8.5.12', '>=');
$phpRows[] = [
'label' => 'PHP version',
'required' => '8.5 or newer',
'status' => $phpVersionOk ? 'ok' : 'err',
'detail' => PHP_VERSION,
'required' => '8.5.12 or newer',
'status' => !$phpVersionOk ? 'err' : ($phpPatched ? 'ok' : 'warn'),
'detail' => $phpVersionOk && !$phpPatched
? PHP_VERSION . ' (security update needed)'
: PHP_VERSION,
];
if (!$phpVersionOk) {
$errors++;
} elseif (!$phpPatched) {
$warnings++;
}

$fileUploads = (bool) ini_get('file_uploads');
Expand Down
Loading