Files
adminandClaude Opus 5.5 cfd6d7a8df
feuerwerk / gate (push) Failing after 5m1s
feuerwerk / suite (push) Canceled after 4m52s
Lock the server down before the editor goes public (#427)
The editor is about to be opened to strangers: a post on Reddit and a
guest access (#428). nginx served the whole workspace and ran every PHP
file in it, the API let anybody guess passwords as fast as they liked,
and one account could fill the disk.

- nginx (shared web stack, outside this repo, backup in
  /root/config-backup-20261015/) hands out only the two pages, dist/,
  public/brand/, the uploads by kind and ending, and api/index.php.
  Everything else under /feuerwerk/ is a 404.
- PHP behind it has display_errors off and no X-Powered-By. Plain HTTP
  forwards to HTTPS, the API refuses it.
- Headers: a Content-Security-Policy without inline code or eval,
  nosniff, frame-ancestors, HSTS, Referrer-, Permissions-, COOP, CORP.
- vite.config.ts writes the same policy into the built pages, so CI
  runs the whole suite under it. Pixi gets by without eval through
  pixi.js/unsafe-eval. The page CSS left its inline style for a file.
- The harness fails a module for anything the policy refuses, from
  Chrome's log and from the page's securitypolicyviolation event.
- LoginThrottle: five wrong passwords from one address within 15
  minutes, and it gets 429 until 15 minutes after the last of them.
  The address is the last X-Forwarded-For entry, the Brandmauer's own.
- Quota: 1 GB per account for uploads, projects and snapshots. Only a
  write that grows the account is refused (413). Admins have no limit.
- A write whose Sec-Fetch-Site says another site is refused (403).
- The session cookie carries Secure, nginx passes HTTPS on.
- showFailure puts the message in as text, not as HTML.
- Every PHP script outside api/ stops unless it runs on the command
  line.
- The video worker sees the checkout read-only and can only write
  data/videos. No capabilities, 2 GB and 256 processes at most.
- tests/php/rights.test.php runs the API under php -S on a scratch
  directory and asks every route as signed out, owner, stranger and
  admin. A route named in api/index.php but missing in the matrix
  fails it. limits.test.php covers the brake and the quota.

Findings:

- tmp/ was readable from outside, with 79 copies of project files.
  Closed on 15.10. at 20:02 with chmod 750. The log back to 11.10.
  shows only our own requests.
- Port 8080 is bound on 0.0.0.0, so the LAN could reach nginx past the
  Brandmauer. Left as it is: the web stack is not this project's.
- npm audit finds nothing in what is shipped. Dev tools (vitest,
  brace-expansion, source-map-js) have advisories, not touched here.

Tests: check 313 files / 4843 tests. PHP limits 21, rights 60,
accounts 56, snapshots 15, releases 20, files 46 (videos 50/51, no
ffmpeg in the web container). e2e on stein2, atx was off: 34+38+41
197/198, the one red check (a click in 41) passes with 41 alone,
27/27. 67 misses frames at 1080p on stein2 with and without this.
Path list from outside: 10 served, 26 refused. ZAP baseline against
the live site: nothing High or Medium under /feuerwerk/.
Counter-checks: rights.test.php without the cross-site guard, the
quota, the brake, the admin check of users, or with an undocumented
route fails each time. A policy without wasm-unsafe-eval fails e2e 41
with the harness's own CSP line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 23:05:32 +02:00

54 lines
2.0 KiB
PHP

<?php
declare(strict_types=1);
// A command-line script: under a web server it does nothing (#427).
if (PHP_SAPI !== 'cli') {
exit(1);
}
/**
* The video worker (#402): converts uploaded videos, one after the other, for as long as it runs.
*
* php tools/video-worker.php [datadir] keeps watching the queue
* php tools/video-worker.php [datadir] --once converts what waits now, then stops
*
* On stein2 it runs in the container `feuerwerk-video` (tools/video/), in CI beside the PHP
* server (tools/ci/serve.sh). Both need ffmpeg and ffprobe on the PATH.
*/
require __DIR__ . '/../api/lib/AtomicFile.php';
require __DIR__ . '/../api/lib/OriginalName.php';
require __DIR__ . '/../api/lib/UserStore.php';
require __DIR__ . '/../api/lib/HashedFileStore.php';
require __DIR__ . '/../api/lib/VideoFormat.php';
require __DIR__ . '/../api/lib/VideoQueue.php';
require __DIR__ . '/../api/lib/VideoStore.php';
require __DIR__ . '/../api/lib/VideoWorker.php';
$args = array_slice($argv, 1);
$once = in_array('--once', $args, true);
$paths = array_values(array_filter($args, static fn(string $a): bool => $a !== '--once'));
$data = $paths[0] ?? dirname(__DIR__) . '/data';
$store = new VideoStore("{$data}/videos");
$worker = new VideoWorker($store, "{$data}/videos");
$interrupted = $store->queue()->requeueInterrupted();
fwrite(STDERR, "[video-worker] watching {$data}/videos/queue" . ($interrupted > 0 ? ", {$interrupted} interrupted job(s) queued again" : '') . "\n");
while (true) {
$job = $store->queue()->next();
if ($job !== null) {
$file = "{$job['folder']}/{$job['hash']}.mp4";
$started = microtime(true);
$worker->runOnce();
$record = $store->record($file);
$status = $record === null ? 'deleted' : ($record['status'] . ($record['status'] === 'failed' ? ": {$record['error']}" : ''));
fwrite(STDERR, sprintf("[video-worker] %s %s in %.1f s\n", $file, $status, microtime(true) - $started));
continue;
}
if ($once) {
break;
}
sleep(1);
}