1.0/mvp #33
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
@clawbot are we ready to put this into prod and tag a 1.0.0, exposed to the whole internet? are we ready? what are we missing? does it have all appropriate prod security headers, all security pitfalls have been checked?
Pre-1.0 Security Audit -- sneak/webhooker
Comprehensive security audit for internet-facing 1.0.0 readiness.
BLOCKERS -- Must Fix Before Internet Exposure
1. No Security Headers (middleware)
File:
internal/middleware/middleware.go(entire file -- missing middleware)File:
internal/server/routes.go(middleware stack, lines 10-40)The application sets zero production security headers. None of the following are present:
Strict-Transport-Security(HSTS)X-Content-Type-Options: nosniffX-Frame-Options: DENYContent-Security-PolicyX-XSS-Protection: 0Referrer-Policy: strict-origin-when-cross-originPermissions-PolicyImpact: Without these, the app is vulnerable to clickjacking (iframe embedding), MIME type sniffing attacks, and lacks defense-in-depth for XSS. HSTS is critical for an internet-facing app to prevent SSL stripping.
Suggested fix: Add a
SecurityHeaders()middleware that sets all headers on every response. Apply it early in the middleware stack inroutes.go. CSP needsunsafe-evalfor Alpine.js andunsafe-inlinefor Tailwind/style blocks. These should be tightened with nonces if possible.The README TODO section confirms this is known unfinished work.
2. No CSRF Protection on Any State-Changing Forms
Files: All form templates and their POST handlers:
templates/login.html-- login formtemplates/sources_new.html-- create webhooktemplates/source_edit.html-- edit webhooktemplates/source_detail.html-- delete webhook, add/delete/toggle entrypoints and targetsinternal/handlers/auth.go-- login handlerinternal/handlers/source_management.go-- all CRUD handlersNone of the 12+ POST forms include a CSRF token. No CSRF middleware exists.
Impact: An attacker can craft a malicious webpage that, when visited by an authenticated user, silently submits forms to create webhooks, add HTTP targets pointing anywhere, delete webhooks, or modify configuration. This is especially dangerous combined with the SSRF issue below -- an attacker could create a target that exfiltrates data from the internal network.
Note:
SameSite=Laxon the session cookie provides partial mitigation -- it blocks cross-site POST requests from some vectors, but Lax is not sufficient for security-critical state-changing operations.Suggested fix: Implement CSRF token middleware (e.g.,
gorilla/csrforjustinas/nosurf). Add a hiddencsrf_tokenfield to every form. Validate on every POST handler.The README TODO section confirms: "CSRF protection for forms" is listed as unfinished.
3. No SSRF Protection -- HTTP Targets Can Hit Internal/Private IPs
File:
internal/handlers/source_management.go,HandleTargetCreate()(line ~497-548)File:
internal/delivery/engine.go,doHTTPRequest()(line ~459-505)When a user creates an HTTP target, the URL is accepted without any validation beyond checking it's non-empty. The delivery engine then makes HTTP POST requests to whatever URL is configured, including:
http://127.0.0.1:*/http://localhost:*-- local serviceshttp://169.254.169.254/-- cloud metadata (AWS/GCP/Azure instance credentials)http://10.0.0.0/8,http://172.16.0.0/12,http://192.168.0.0/16-- internal networksfile://,gopher://, etc. -- other URL schemesImpact: A user (or an attacker who gained access via CSRF) can use webhooker as a proxy to scan internal networks, access cloud metadata services, or attack internal services. This is a critical vulnerability for any internet-facing service that makes outbound HTTP requests to user-controlled URLs.
Suggested fix:
http://orhttps://, must not resolve to private/reserved IP ranges.doHTTPRequest()-- resolve the hostname, check if the IP is in a private range, and reject if so. Use a customnet.Dialerwith aControlfunction that blocks private IPs.4. No Rate Limiting on Login Endpoint
File:
internal/handlers/auth.go,HandleLoginSubmit()(lines 28-89)File:
internal/server/routes.go, line 52There is no rate limiting, account lockout, or delay on failed login attempts. An attacker can make unlimited login attempts at full speed.
Impact: Trivial brute-force attacks against user passwords. Even with Argon2id (which provides some computational cost per attempt), the lack of any rate limiting means an attacker can try millions of passwords.
Suggested fix:
5. Session Fixation Vulnerability -- No Session Regeneration on Login
File:
internal/handlers/auth.go,HandleLoginSubmit()(lines 74-82)After successful password verification, the handler calls
h.session.Get(r)to retrieve the existing session, sets user info on it, and saves it. The session ID (cookie) is never regenerated.Impact: If an attacker can set a session cookie before the victim logs in (via XSS, network sniffing on HTTP, or subdomain cookie injection), the attacker's cookie remains valid after login, giving them access to the authenticated session.
Suggested fix: After successful authentication, destroy the old session and create a new one. gorilla/sessions doesn't have a built-in "regenerate" method, so destroy the old session, save to clear the cookie, then get a new session and set user info.
SHOULD-FIX -- Important for Production Hardening
6. No Target URL Validation
File:
internal/handlers/source_management.go,HandleTargetCreate()(line ~530)The only check on the URL is
url == "". There's no validation that it's a well-formed URL, uses an allowed scheme (http/https only), or that the hostname is not an IP literal in a private range.7. User Profile Route Missing Auth Middleware
File:
internal/server/routes.go, lines 62-64The
/user/{username}route is not wrapped withs.mw.RequireAuth(). The handler checks auth internally, but this is defense-by-implementation rather than defense-by-design.Suggested fix: Add
r.Use(s.mw.RequireAuth())to the route group.8. No Request Body Size Limit on Form Endpoints
File:
internal/handlers/auth.go,internal/handlers/source_management.goThe webhook handler properly limits body size to 1MB, but the login form, create form, edit form, and all other POST handlers call
r.ParseForm()without setting a body size limit.Suggested fix: Wrap
r.Bodywithhttp.MaxBytesReader(w, r.Body, maxFormSize)before callingParseForm(), or add a globalMaxBytesReadermiddleware for non-webhook routes.9. Admin Password Logged as Structured Log Field
File:
internal/database/database.go, line 133The initial admin password is logged via
d.log.Info("admin user created", "password", password, ...). In production with structured JSON logging, this password will be written to wherever logs are shipped.Suggested fix: Print the password to stderr directly (not via slog) with a clear banner, or use a separate output channel.
10. No Inactivity-Based Session Timeout
File:
internal/session/session.go, lines 79-82Sessions have a 7-day
MaxAgebut no activity-based expiration. A session remains valid for the full 7 days regardless of whether the user has been active.11. No Cache-Control on Authenticated Pages
Authenticated pages don't set
Cache-Control: no-store. Browsers and proxies may cache these pages, potentially exposing sensitive webhook configuration and logs.12. WriteTimeout (10s) Conflicts with Middleware Timeout (60s)
File:
internal/server/http.go, line 13 --WriteTimeout: 10 * time.SecondFile:
internal/server/routes.go, line 31 --middleware.Timeout(60 * time.Second)The HTTP server's
WriteTimeoutof 10 seconds will kill connections before the 60-second middleware timeout fires.NICE-TO-HAVE -- Defense in Depth
13. No Webhook Signature Verification
The webhook receiver doesn't verify HMAC signatures from senders. Listed in the TODO. Not strictly required for a store-and-forward proxy, but important for validating webhook authenticity.
14. No Per-Webhook Rate Limiting in Receiver
Listed in the README TODO. Without this, a misbehaving sender could flood a webhook with events.
15. No Automatic Event Retention Cleanup
The
retention_daysfield exists but automatic cleanup is not implemented. Per-webhook databases will grow unbounded.16. Password Change Flow Not Implemented
Currently there's no way to change the admin password through the UI.
What's Already Good
html/template-- automatic context-aware escaping, no XSS. No use oftemplate.HTMLor other unescaped types.Where()calls use parameterized?placeholders.subtle.ConstantTimeCompare.HttpOnly: true,Secure: truein prod,SameSite: Lax.webhookeruser.Access-Control-Allow-Origin: *in prod mode.docker build .passes -- CI gate is green.Dependency Check
Go modules are reasonably current. Key dependency versions:
golang.org/x/crypto v0.38.0-- current, no known CVEsgorilla/sessions v1.4.0-- currentgorilla/securecookie v1.1.2-- currentgorm.io/gorm v1.25.5-- currentgo-chi/chi v1.5.5-- currentsentry-go v0.25.0-- currentprometheus/client_golang v1.18.0-- slightly old but no security CVEsNo critical CVEs identified in the dependency tree.
Verdict: NO-GO
webhooker is not ready for internet-facing 1.0.0 deployment. The codebase has a solid foundation (good architecture, proper password hashing, template escaping, parameterized queries, non-root container), but it is missing five critical security features that the README itself acknowledges as TODO:
Items 1-4 are explicitly listed as unfinished in the README TODO. Item 5 is a code-level vulnerability. All five are standard expectations for any internet-facing authenticated web application.
Minimum for 1.0: Fix all five BLOCKER items. The SHOULD-FIX items (especially #6 URL validation, #8 body size limits, #9 password logging) should also be addressed but are lower priority.
Suggested Issues to File
Security Audit Complete — Issues Created
Created 7 tracking issues from the audit findings above:
Blockers (must-fix before internet exposure):
Should-fix:
Verdict: NO-GO for internet-facing 1.0.0. 5 blockers must be resolved first. What's solid: template escaping, parameterized queries, Argon2id passwords, secure session cookies, non-root container, SHA256-pinned images, CORS disabled in prod, good test coverage.
Add
botlabel to issues you want automated.#40 is closed bc its intentional. fix all the others. add the bot label and do it.
1.0 readiness — re-audit (2026-08-07)
Re-audited
origin/mainagainst the March security audit above. The five blockers and the should-fix items you asked to fix are all done and verified in the current code, so the March NO-GO no longer stands. What remains for 1.0 is a tight, fully-tracked set, collected under the new1.0.0milestone.Verified resolved since March
SecurityHeaders()sets HSTS,X-Content-Type-Options: nosniff,X-Frame-Options: DENY, CSP,Referrer-Policy,Permissions-Policy, applied globally (#34)CSRF()middleware on every state-changing route group, with tests (#35)internal/delivery/ssrf.goplus tests (#36)LoginRateLimit()on the login routes (#37)session.Regenerate()is called on login inauth.go(#38)MaxBodySize()on all form route groups (#39)html/templateescaping, and real test coverage across the security-critical packages.Still required for 1.0 —
1.0.0milestoneEach is small and carries a definition of done:
/user/{username}inRequireAuth(today it is handler-only)Cache-Control: no-storeon authenticated pagesWriteTimeoutagainst the 60s request-timeout middleware (the 10s deadline currently wins, so the 60s timeout is dead)RetentionDays(a reaper; the field exists but per-webhook DBs currently grow unbounded)Please confirm in-or-out for 1.0 (3 items)
/webhook/{uuid}receiver. Recommend IN: it is the one unauthenticated, internet-facing surface and pairs with #63 for growth/abuse control.Post-1.0 backlog (filed, not blocking)
Housekeeping
schema_migrationsinto000.sql) — not applicable: webhooker has noschema_migrationstable at all (pure GORMAutoMigrate). Recommend closing; it has been assigned to you since July with this same finding.Decisions for you
1.0.0and expose it, or is there anything else you want on the bar?botlabel as before, or hand-pick which ones to automate?Assigning to you for the scope and go/no-go call.
Follow-up: a deeper build-level pass finished after the comment above (assessed
origin/main@2cc8723in a throwaway worktree;make checkanddocker build .are both GREEN — the CI-pinnedgolangci-lintv2.11.3 stays clean, host v2.12.1 shows 9 pre-existinggoconstfindings only).It surfaced three more evidence-backed items, now filed and added to the
1.0.0milestone:buildSlackTargetConfigskipsValidateTargetURL, so Slack target URLs are only guarded at request time, not at creation (SSRF parity gap).clientForConfig()drops the SSRF-safe Transport when a per-target timeout is set; latent today (UI can't set one) but a real bypass to close.databaseandlogtarget types are selectable in the UI but their handlers are no-ops (deliverDatabase()just marks the delivery delivered — the design rejected in #43). 1.0 should not offer target types that do nothing.I also concretized #57 with the specific UI problems found (terminology split, placeholder Profile settings, raw JSON config rendered in the UI, dead target options, misleading retention copy).
Net: no new hard blocker beyond the milestone, and the build is green. The scope and go/no-go decisions in my previous comment still stand — the milestone is now the complete
1.0.0picture.Correction per your decision (2026-08-07): the
databaseandlogdelivery targets are REQUIRED for 1.0, so nothing gets hidden. #70 is reframed from "hide the no-op options" to "implement thelogtarget"; database archiving remains #43. The1.0.0milestone is otherwise unchanged.Session handoff — 1.0 status (2026-08-07)
Where webhooker 1.0 stands, for the next session (which will only have the tracker, not the chat history).
Merged to main this session (all independently reviewed)
/userRequireAuth), #62 (WriteTimeout vs middleware timeout), #68 (Slack create-time SSRF validation), #69 (SSRF-safe client on per-target timeout) — first hardening wave.MaxRetries-gated retries.Open PRs
clawbot). The archive mechanics (separatearchive-{webhookID}.db, close/reopen debounce, auto-recreate, optional expiry, prune-on-open) are solid and reviewed, but ONE required fix is pending and NOT yet applied (the rework was interrupted): a failed archive must record aFaileddelivery rather than silentlyDelivered. Full instructions are in the last two comments on PR #84.1.0.0 milestone — still open
routes.go+ratelimit.go+config.go).Follow-ups filed (triage for scope)
RetentionDayscan't be set to 0 (retain forever) via the normal create path.envInt, etc.) should fail loudly on set-but-unparseable values (extends the fix already merged for the duration parser).retryingdeliveries whose target type was changed to a non-retry type (narrow edge case).Housekeeping
schema_migrationsinto000.sql) — not applicable: webhooker uses GORM AutoMigrate and has noschema_migrationstable. Recommend closing.Decisions still needing you
Working method for the next agent
One small unit at a time: a Gitea issue with a definition of done → a subagent (rooted at the clone, isolated git worktree from
origin/main) implements it and opens a PR, validated bydocker build .(host Go is 1.25 butgo.modneeds 1.26, so Docker is the authoritative gate —make checkwon't run on the host) → an independent adversarial review → rework driven via PR comments until clean → assign @sneak to merge. Parallel units must touch non-overlapping files. CI pinsgolangci-lintto an older version than a current host may have, so host-onlygoconstwarnings in untouched files are pre-existing, not blockers.