The frontend image kept shipping vulnerable OS packages (nginx 1.28.3-r1,
curl/libcurl 8.19.0, c-ares 1.34.6) despite the apk upgrade line, for two
independent reasons:
1. The runtime stage's apk upgrade layer was cached indefinitely — the
CACHEBUST build-arg CI passes (github.run_number) was only declared in
the builder stage, and ARGs don't cross stage boundaries. Both
Dockerfiles now redeclare CACHEBUST in the runtime stage and consume it
in the apk RUN, so every build re-runs the upgrade and picks up current
Alpine security updates.
2. nginx itself can never upgrade via apk on the nginx.org-based image:
the bundled nginx-module-* packages pin the exact nginx version, so
Alpine's patched 1.28.3-r4 is unreachable (verified empirically —
apk add --upgrade nginx is a silent no-op). nginx fixes must come via
the base tag, so bump to nginx:1.30-alpine (current stable, 1.30.4 on
Alpine 3.24, same nginx.org conf.d layout — drop-in).
Verified: local image build scans clean with Trivy (0 OS findings, was 21);
container serves /health, SPA fallback, and BRAND_TITLE envsubst as non-root
nginx user.
Closes code-scanning alerts 371-374, 376-392 (nginx HTTP/2 & module CVEs,
curl CVE-2026-5773/-6276 + 6 medium, c-ares CVE-2026-33630).
Closes Trivy alerts #375 (sigstore CVE-2026-48815), #321 (@sigstore/core),
#314 (tar) — npm@10 bundles the vulnerable sigstore 3.1.0; npm 11 ships the
patched 4.x. Safe because this npm is CLI-only in the final image: runtime
deps come from the builder stage's node_modules and the entrypoint runs node,
not npm, so the install-behaviour issues that motivated the 10.x pin never run
here. npm 11 requires Node >=22.9 — satisfied by node:22-alpine.
Repo transferred from the-luap/picpeak → PicPeak/picpeak. Docker images
publish to ghcr.io/picpeak/picpeak/{backend,frontend} (lowercase, per the
GHCR canonical form computed by docker-build.yml's `${GITHUB_REPOSITORY,,}`).
Sweep covers:
- docker-compose.production.yml + Dockerfiles → new image registry path
- README, CONTRIBUTING, SECURITY, SIMPLE_SETUP, scripts/picpeak-setup.sh
→ new GitHub URLs
- Update-check / release-notes services (updateCheckService,
environmentService, updateNotificationService, adminSystem,
UpdateNotification, githubReleaseUrl) → GitHub API + tag URLs use the
canonical PicPeak/picpeak path
- Issue templates + README-DOCKER + workflow README → updated package URLs
- One commit-context comment in migrations/090 + customerAccountsService
CHANGELOG.md is intentionally untouched (historical release entries are
immutable; GitHub auto-redirects the old URLs indefinitely).
CLAUDE.md keeps the bare `(the-luap)` reference — that's the maintainer's
personal handle, not a repo URL.
22 files, 48/48 line swaps (every change is a 1:1 URL replacement).
Security hardening for inbound supplier-invoice previews. The admin UI no
longer renders raw PDFs — a malicious inbound PDF could otherwise run embedded
JS or phone home in the admin's session. Instead PDFs are rasterised to flat
PNGs server-side and only those images are shown.
- backend: new rasterizeService shells out to poppler `pdftoppm` (added to the
Docker image via apk poppler-utils — an OS package, NOT a Node PDF lib, so it
respects the pdfkit+pdf-lib "no third PDF lib" rule). pdftoppm executes no JS
and fetches no remote resources, so it doubles as the SSRF/phone-home guard.
Rendered pages cached under storage/business-docs/inbound/rendered/<id>/.
- GET /inbound/:id/page/:n streams the rasterised PNG (CSP default-src 'none'
+ nosniff). GET /inbound/:id/file now serves PDFs as a DOWNLOAD only
(Content-Disposition: attachment) — never inline; images still inline.
- frontend: triage preview switched from a raw-PDF <iframe> to rasterised page
images (getInboundPageBlob), defaulting to the LAST page (QR-bill) with
prev/next nav for multi-page PDFs; images stream as before.
- i18n: previewError / prevPage / nextPage / pageOf (EN + DE).
REQUIRES A BACKEND IMAGE REBUILD (Dockerfile adds poppler-utils) — a plain
`docker compose pull` of a stale image won't have pdftoppm; the route then
returns 503 RASTERIZER_UNAVAILABLE and the UI shows "preview unavailable".
Verified: node -c, a pdfkit->pdftoppm rasterise smoke test (renders + caches),
en/de JSON valid, npm run build green.
The runtime image (node:22-alpine) shipped without any fonts, so when
sharp/librsvg rasterised an SVG logo containing live <text> for the CRM
PDFs, the vector artwork drew but the text rendered as tofu boxes - a
'corrupted' logo on invoices/quotes.
- Add fontconfig + DejaVu/Liberation (broad Unicode fallback) and refresh
the font cache.
- Register picpeak's own bundled brand fonts (assets/fonts/<Family>/*.ttf -
the same files PDFKit and the web UI already use) with fontconfig via a
conf.d <dir> entry + fc-cache, so the logo's text renders in its ACTUAL
brand typeface rather than a generic fallback.
The fresh-install restart loop reported by @MrGabri (and confirmed by
@AloePacci with the user:0:0 workaround) had a clear root cause:
- Dockerfile pinned USER nodejs (UID 1001) before the entrypoint
ran, so the existing chown branch in init-production.sh:13 was
dead code.
- wait-for-db.sh (the actual entrypoint, not init-production.sh)
silently swallowed mkdir/EACCES on bind mounts with || true,
then a downstream migration error surfaced as the visible failure.
- Net effect on a typical Linux host where the bind-mount dir is
owned by UID 1000: container can't write, exits non-zero,
restarts forever with no clear error.
Switch to the standard Docker drop-privileges pattern:
1. Install su-exec, drop `USER nodejs` from the Dockerfile —
container now starts as root.
2. wait-for-db.sh: if running as root, chown /app/storage,
/app/data, /app/logs to nodejs and re-exec self via
su-exec nodejs:nodejs. App still ends up running as UID 1001.
3. Preflight check for non-root invocations (compose `user:`
overrides): verify the bind mounts are actually writable
before continuing. If not, exit 1 immediately with an
actionable error pointing at the docs — no more silent
restart loops.
Also:
- Delete backend/init-production.sh. It was an orphan — no caller
in the Dockerfile, compose, or anywhere else. Its chown logic
looked authoritative enough that @MrGabri ran it manually trying
to debug, which is what finally surfaced the EACCES.
- docker-compose.yml: drop user: + PUID/PGID env. The pattern-B
UID-matching workaround they implemented is obsolete now that
pattern A (root-then-drop) is in place.
- .env.example + README: drop PUID/PGID documentation.
- Add fresh-install smoke test workflow. Boots backend + postgres
against bind mounts owned by UID 1000 (the GitHub runner UID,
and the common-mismatch case on Linux hosts) and verifies:
+ container reaches healthy without restart-looping
+ chown happened (dirs now owned by 1001 inside the container)
+ node runs as nodejs, not root (su-exec drop worked)
+ /health returns status:ok
+ with --user 5005:5005 + unwritable mounts, preflight exits
loud with the expected error string
Verified locally end-to-end against a fresh Postgres + UID-501-owned
bind mount: backend reaches healthy in ~20s, chown applied, node
runs as nodejs, no restart loop. Docs in picpeak-docs cover the new
behavior + a Troubleshooting section for the install-path bugs
fixed in #484/#494/#511/#488.
Refs: #484
Triage of an external SAST/SCA scan run on 2026-05-06. Most loud findings
were already resolved by PR #412 (the 18-CVE backport); this PR addresses
the residual real items:
* Drop unused `handlebars` from backend deps. The runtime require was
removed in PR #367 (#367) but the package.json line stayed. handlebars
was the source of two flagged criticals (CVE-2026-33937 RCE,
GHSA-2w6w-674q-4c4q AST injection) plus 8 highs — all now gone.
* `npm audit fix` on backend + frontend. Bumps transitive picomatch,
flatted, postcss, brace-expansion via lockfile, and direct dompurify,
lodash, vite, i18next-http-backend within their existing semver ranges.
Both audits now report 0 vulnerabilities.
* Add `event.origin === window.location.origin` check to the THEME_PREVIEW
message listener in PreviewPage. The branding page posts from the same
origin, so nothing legitimate is rejected; without the check, any third
party that window.open()'d the preview could push arbitrary
branding/theme payloads (semgrep
insufficient-postmessage-origin-validation).
* nginx: `proxy_hide_header` for X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, Content-Security-Policy, Permissions-Policy,
Strict-Transport-Security at server level. nginx adds these itself, but
helmet on the backend was also emitting them — clients were seeing
duplicates (testssl flagged "Multiple X-Frame-Options / CSP /
Permissions-Policy / Referrer-Policy headers" on the live origin).
Single source of truth now.
* Dockerfile hardening (checkov):
- HEALTHCHECK on backend/Dockerfile, backend/Dockerfile.dev,
frontend/Dockerfile.dev. Frontend production Dockerfile already had
one.
- USER node in frontend/Dockerfile.dev (was running as root).
* GitHub Actions docker-build.yml: explicit top-level
`permissions: contents: read`. Per-job blocks already declare
`packages: write` where needed; this stops future steps from
inheriting unintended privileges (CKV2_GHA_1).
Backend npm audit: 4 vulns -> 0.
Frontend npm audit: 6 vulns -> 0.
Backend unit tests: 13 suites, 131/132 passing (1 pre-existing skip).
Frontend type-check + lint: clean.
The pre-existing integration-test failures (live DB / S3 required) and
the ThemeCustomizerEnhanced QueryClientProvider failures are unrelated
and reproduce on origin/beta without these changes.
Video uploads on production fail with "missing ffmpeg" because the
backend container ships nothing usable for the video pipeline.
Two compounding causes:
1. **Alpine + glibc mismatch.** The npm `@ffmpeg-installer/ffmpeg`
dependency added with the video-support PR (commit 68a9dc5)
ships per-platform binaries via optionalDependencies. The Linux
binaries are built against glibc, but the backend image runs on
`node:22-alpine` (musl libc) — known to either fail to execute
or fail on shared-library lookups on Alpine.
2. **`ffprobe` missing entirely.** `@ffmpeg-installer/ffmpeg`
bundles only the `ffmpeg` binary. There's a separate
`@ffprobe-installer/ffprobe` package that the codebase never
depended on. But `videoProcessor.js:21` calls
`ffmpeg.ffprobe(videoPath, …)` — the very first step of the
video pipeline shells out to a `ffprobe` binary that doesn't
exist in the image. Even if (1) worked, every video upload
would 500 here.
The fix is to install Alpine's `ffmpeg` package via apk. It ships
both `ffmpeg` and `ffprobe` built natively against musl, ~70MB
extra image size, single line in the Dockerfile, no per-arch
handling needed (apk pulls the right binary for both linux/amd64
and linux/arm64 — works with the multi-arch infra from #349).
- `backend/Dockerfile`: add `ffmpeg` to the apk install line.
- `backend/Dockerfile.dev`: same for dev parity.
- `backend/src/services/videoProcessor.js`: remove the
`setFfmpegPath(require('@ffmpeg-installer/ffmpeg').path)` line
— without removing it, fluent-ffmpeg would prefer the broken
bundled binary over the working apk one. Letting fluent-ffmpeg
fall back to PATH lookup picks up the apk binary in the
container and the developer's locally-installed binary on dev
hosts (Homebrew on macOS, apt on Debian).
- `backend/package.json`: drop the now-unused
`@ffmpeg-installer/ffmpeg` dependency. `npm install` removes
2 packages from the lockfile.
Verified: `videoProcessor.js` still loads cleanly (`node -e
"require('./src/services/videoProcessor')"`); lint clean.
npm@latest resolves to v11 which has a broken promise-retry dependency
on Node 22 Alpine, causing Docker builds to fail. Pin to npm@10 which
stays compatible with the Node 22 base image.
- Upgrade multer to 2.1.1 (CVE-2026-3520, DoS via malformed requests)
- Update tar override to >=7.5.11 (CVE-2026-31802, CVE-2026-29786)
- Upgrade Node base image from 20-alpine to 22-alpine to fix npm
bundled tar/minimatch CVEs in the Docker image
Upgrade npm to latest version in both backend and frontend Dockerfiles
to fix the command injection vulnerability in glob's CLI (CVE-2025-64756).
The vulnerability exists in npm's bundled glob package (< 10.5.0 or < 11.1.0).
Original: feat: enhance security logging and ensure rate limit blocks are properly tracked
- Add comprehensive logging for rate limit blocks with full request details
- IP address (with proper proxy detection), user agent, headers, timestamps
- Rate limit info (current count, limit, remaining, reset time)
- Separate tracking for auth vs general endpoints
- Enhance authentication failure logging
- JWT validation failures with detailed error info
- Admin auth attempts without token
- Failed token validation with user context
- All events include IP, path, method, user agent
- Improve Winston logger configuration for production
- Add automatic log rotation (10MB errors, 50MB combined)
- Create separate security.log for auth/rate limit events
- Ensure logs directory exists automatically
- Add structured JSON format for log aggregation
- Support container logging with LOG_TO_CONSOLE env var
- Create comprehensive documentation
- Security logging guide with examples
- Monitoring recommendations
- Configuration reference
- Add test script to verify logging functionality
All rate limit settings remain configurable via admin panel:
- Window duration, max requests, auth limits
- Skip authenticated requests option
- Public endpoints only option
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>