security
Technical notes on web development, DevOps, and AI integration.
54 articles
- 21:32security
Hardening the Next.js Image Optimizer Against SSRF
A wildcard hostname in remotePatterns turns /_next/image into an open server-side fetch proxy. The close-out pattern: local-only, one explicit host, a negative test.
TL;DR: KotaPortal's Next.js image config used a wildcard hostname in remotePatterns, turning the /_next/image endpoint into a potential SSRF vector since it fetches any HTTPS URL server-side. The fix removed the wildcard, allowing only same-origin images plus one explicit Google avatar host in both remotePatterns and CSP. A negative end-to-end test now verifies unlisted hosts get rejected.
#nextjs#next-image#ssrf - 12:58backend
A Deleted User Is an Authentication Failure: 401, Not 500
A cryptographically valid JWT can still name a user who no longer exists. Mapping sql.ErrNoRows to 401 lets the frontend purge the stale session.
TL;DR: A deleted user's JWT kept hitting the API, and since the middleware treated missing rows as a generic DB error, users got stuck in a 500 banner loop instead of being logged out. The fix uses errors.Is(err, sql.ErrNoRows) to return 401, letting the frontend purge the stale session. Tests now pin both paths.
#go#golang#jwt - 05:30security
A Mechanical Security Net Designed for Silence
A secret-scanning hook that separates findings from errors, non-blocking CI scanners, and a weekly ZAP baseline that skips gracefully.
TL;DR: gitleaks uses exit code 1 for both real leaks and scan errors, so naive hooks block commits over environment failures and get disabled. The fix: block only when exit 1 comes with a valid JSON report containing findings, warn otherwise. CI and weekly ZAP scans collect findings as reports instead of failing builds.
#security#gitleaks#github-actions - 01:35security
When the Server Fails Closed, the Interface Must Tell the Truth
The server refused free uploads from scoped editors while the UI kept offering the button. Gating the picker fixed the affordance, not the security.
TL;DR: Scoped editors could see KotaPortal's media upload button but always hit a 403, since only admin-global accounts could upload free. The fix gates the button, file input, and handler behind the same server-side permission check, so the UI stops promising what the server refuses. Lesson: hiding UI is hygiene, not enforcement; the server remains the real gate.
#rbac#react#ux - 01:34security
Guard the Contents, Not the Container: Two-Sided Rebind Media
The endpoint guarded the target entity but never the source media ids. After a rebind, managing someone else's media became legitimate.
TL;DR: An editor rebinds another bidang's media into their own gallery because the endpoint guarded only the target, not the source. The fix resolves media modules in one parameterized batched query and rejects out-of-scope ids with a static 403 before any write. A similar unguarded path in announcements shows source objects need the same object-level checks on every write path.
#security#rbac#idor - 00:24security
An Audit Trail That Survives Until Someone Asks
An append-only audit table written after each successful mutation, with no foreign key so rows outlive deleted accounts.
TL;DR: A security review found the app couldn't answer who changed a record, since app logs rotate and binlogs lack actor context. The fix is a dedicated audit_logs table written after successful admin mutations, append-only, with indexes and best-effort inserts that never break requests. Residual risk: a failed insert leaves a silent gap; metrics and alerting remain future work.
#audit-log#go#mysql - 00:24security
The ZAP Decision That Ships With Every Closed Row
A passive ZAP baseline is cheap to re-run and proves nothing per issue, so each closed row must carry one explicit decision token.
TL;DR: KotaPortal's post-RBAC review found closed cards assumed to need a ZAP scan each, wasting pipeline time since passive baseline scans prove nothing new. The team now requires one decision token per row: ZAP:rescan with a valid path, or ZAP:none with a mandatory reason. Enforced by a small lint, this turns silent security bypasses into deliberate, auditable choices.
#security#owasp#zap - 23:28security
Error Bodies That Repeat Your Input Back
API error messages that echo requester input are a cheap reconnaissance channel. The fix: specific code, static detail, regression tests.
TL;DR: KotaPortal's admin API was reflecting user input back in error messages across five spots in two services, confirming injection viability per OWASP guidance. The fix keeps specific error codes but makes human-readable details fully static, never echoing request data. Each endpoint gets a regression test that sends a random probe and fails if the probe appears in the response.
#api#security#http - 13:50backend
From Full Buffering to Streaming Uploads in Go
Five slots times 10 MiB per request is a DoS invitation. A per-user rate limit plus multipart streaming cuts memory to roughly 1 MiB per request.
TL;DR: Buffering five 10 MiB uploads per request risked gigabytes under concurrent load, since a per-request cap doesn't limit request frequency. A per-user rate limiter of 30 requests per minute returns 429 with Retry-After to cap waves. Streaming with a 1 MiB memory window keeps each request near 1 MiB regardless of file size.
#go#security#upload