Independent code audit #303
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?
Full read of
main@4f5ecb1. One line per finding, grouped by severity.Unfit for purpose
cache=shared, nobusy_timeout, no WAL, and an unboundeddatabase/sqlpool (internal/database/database.go:140,internal/database/webhook_db_manager.go:237); the ingest handler's write transaction (internal/handlers/webhook.go:223) races engine workers writing results/status to the same per-webhook file (internal/delivery/engine.go:761,internal/delivery/engine.go:778), so under any concurrency you get immediateSQLITE_BUSY/SQLITE_LOCKED→ 500s to senders and deliveries stuckpending(the write errors are only logged). No test exercises concurrent writers.Notifydrops tasks when the channel is full and the only recovery forpendingrows is process restart (internal/delivery/engine.go:174-181); the periodic sweep handlesretryingonly (internal/delivery/engine.go:572-602), so "guaranteed delivery" is not true without a restart.Significant
Allow()returns false butCooldownRemaining()returns 0 (internal/delivery/circuit_breaker.go:71-73,:85-87), socircuitBreakerBlockreschedules with delay 0 (internal/delivery/target_http.go:145-165) and every queued task for that target spins throughretryChwith a DB status write per iteration for the whole probe duration (up to 30s).True-Client-IP/X-Real-IP/X-Forwarded-Forunconditionally (internal/middleware/ratelimit.go:75), so the limit is bypassed by setting a header; each attempt costs a 64 MB argon2 hash (internal/database/password.go:18), which makes/pages/logina trivial memory-exhaustion DoS. Violates REPO_POLICIES "never trust X-Forwarded-For unconditionally".WEBHOOKER_ENVIRONMENTdefaults todev(internal/config/config.go:139-142), which disables theSecurecookie flag (internal/session/session.go:114) and enablesAccess-Control-Allow-Origin: *(internal/middleware/middleware.go:143); an operator who forgets the variable ships an insecure prod. Default should be prod.internal/server/http.go:44-50); nothing callsfx.Shutdowner, so the process stays up with no listener instead of exiting non-zero.internal/handlers/profile.go:116), logout is client-side only (internal/handlers/auth.go:206), and a stolen cookie is valid for 7 days (internal/session/session.go:112).internal/delivery/ssrf.go:41-59, enforced at creationinternal/handlers/source_management.go:948and at dialinternal/delivery/ssrf.go:206); a self-hosted dispatcher that cannot deliver to RFC1918/LAN targets is missing its primary use case. Needs an allowlist (e.g.ALLOWED_TARGET_CIDRS).README.md:174); the UI also shows no per-attempt results (status code, error, response body, duration) —templates/source_logs.html:26-30renders only the delivery status string, so the "full request/response logging" claim is unobservable.Minor
X-Forwarded-Protois trusted from any client (internal/middleware/csrf.go:19,internal/handlers/source_management.go:314).RetentionDays <= 0,internal/database/retention.go:143) is unreachable: create/edit parsers ignorev <= 0(internal/handlers/source_management.go:162,:457) and the forms usemin="1"(templates/source_edit.html:31).internal/delivery/engine.go:239,internal/delivery/target_http.go:358), so a shutdown mid-request records a failed attempt and burns a retry;time.AfterFuncretry timers are never cancelled and outlive the engine (internal/delivery/engine.go:199).2^(n-1)sup to2^30 s(internal/delivery/target_http.go:266-271); the form allowsmax_retries=20(templates/source_detail.html:110), i.e. a 6-day gap before the last attempt.attemptNum >= maxRetries(internal/delivery/target_http.go:178) makesmax_retries=Nmean N total attempts, not N retries; README/UI say retries.internal/handlers/source_management.go:513-521), does not removearchive-{id}.db(internal/database/webhook_db_manager.go:166), leaves a stalearchiveWritercached (internal/delivery/target_database.go:130), and closes thesql.DBthat engine workers may still hold (internal/database/webhook_db_manager.go:155-162).internal/middleware/middleware.go:125) and the webhook handler logs the entrypoint UUID again (internal/handlers/webhook.go:42-46); since the UUID is the credential, logs carry all credentials.sql.DBpools are never bounded or closed; fds scale with webhook count, and the retention sweep opens every webhook DB hourly and never releases (internal/database/webhook_db_manager.go:241,internal/database/retention.go:163).deliveries.status,deliveries.event_id,delivery_results.delivery_id,events.created_at(internal/database/model_delivery.go:18-20,internal/database/model_delivery_result.go:7,internal/database/model_event.go:7); recovery, sweeps, retention and pagination are full scans.reapExpiredruns three deletes outside a transaction (internal/database/retention.go:204-252) and can delete events whose deliveries are in flight.sessions.NewCookieStore(keyBytes)with one key,internal/session/session.go:107) while README claims encrypted cookies (README.md:210)./metricsis registered when onlyMETRICS_USERNAMEis set (internal/server/routes.go:73); an emptyMETRICS_PASSWORDis accepted as the credential (internal/middleware/middleware.go:224).internal/handlers/auth.go:105-117).Cookie/Authorization(internal/delivery/target_http.go:444-454), and always POSTs regardless of the captured method (internal/delivery/target_http.go:360);User-Agentis hardcodedwebhooker/1.0(:489) rather than the build version.internal/handlers/webhook.go:282-289), which replay/audit will need.::/128,64:ff9b::/96(NAT64),ff00::/8,2001:db8::/32(internal/delivery/ssrf.go:41-59); dial uses onlyips[0]with no fallback (:220).gorm.io/driver/sqlitedrags inmattn/go-sqlite3and forces CGO + a Debian builder (go.mod:20,Dockerfile:3,Dockerfile:51) whilemoderncis what runs;github.com/glebarez/sqliteremoves CGO entirely.github.com/go-chi/chi v1.5.5(go.mod:8) is the unmaintained pre-v5 line.script/testalways runs-vand omits-cover(script/test:9), contrary to the conditional-verbose-rerun pattern in REPO_POLICIES.APIKeymodel (internal/database/model_apikey.go),Target.MaxQueueSize(internal/database/model_target.go:29), empty/api/v1route group (internal/server/routes.go:63), no-opOnStart(internal/handlers/handlers.go:112-116),Server.exitCode/cleanupForExit/MaintenanceMode(internal/server/server.go:100,:158)./webhook/{uuid}acceptsANYmethod (README.md:713) but the handler is POST-only (internal/handlers/webhook.go:24); README Docker section saysgolang:1.24(README.md:920);TODO.md"Next Step" is retention cleanup, already shipped in #78 (TODO.md:21), and lists password change as future, shipped in #83 (TODO.md:75).Filed as individual issues: #304 #305 (unfit for purpose), #306 #307 #308 #309 #310 (significant). Skipped per sneak: login rate-limit header spoofing / argon2 DoS and session invalidation/revocation — single admin user with a long random password; good enough for now. Minor findings remain here untracked.