Files
web-hosts/domains/coppertone.tech/docs/audits/20251120-160733-security-audit.md
2025-12-26 13:38:04 +01:00

72 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copper Tone Technologies Security/Quality Audit
**Date:** 2025-11-20
**Auditor:** Codex (manual review per `docs/AUDITOR.md` guidance)
**Scope:** Backend Go services, database migrations, frontend auth/XSS posture, infra (Containerfiles, podman-compose), CI workflows.
**Method:** Static code/config review only. Automated scans (npm audit, govulncheck, trivy, lint/tests) **not run** because this session uses a restricted, offline sandbox.
---
## Executive Summary
- **Overall risk: High.** Users can self-assign `ADMIN` at registration, downstream services do not enforce ownership/role checks, and the Stripe webhook has no signature verification. These allow total compromise of data and payments.
- **Transport security gaps:** All services hardcode `sslmode=disable` to Postgres; no way to require TLS in production.
- **Auth inconsistencies:** JWT claims differ between issuer and consumers (`userId` vs `user_id`), enabling logic bugs while RBAC already trusts client-chosen roles.
- **Frontend XSS exposure:** Markdown is rendered and injected via `v-html` with no sanitization.
- **DevOps hygiene:** Compose ships default secrets; containers run as root (scratch images) without CA certs; no backend `.env.example`.
---
## Critical Findings
- **Privilege escalation at signup** `handleRegisterEmailPassword` and `handleRegisterBlockchain` accept a user-supplied `role` and insert it directly (`backend/functions/auth-service/main.go:168-263`). Any new user can register as `ADMIN`, then call other services with administrative privileges.
- **No authorization / data-owner checks** Work Management and Payment services allow any authenticated token to list/read/update/delete all projects, tasks, work orders, invoices, and payments; no scoping to the caller or role gating beyond minimal wrappers (`backend/functions/work-management-service/main.go:69-360`, `payment-service/main.go:97-454`). Combined with self-assigned admin, this is full data disclosure/modification risk.
- **Stripe webhook unauthenticated** `/webhooks/stripe` has TODOs for signature verification and status updates but currently accepts any POST and returns 200 (`backend/functions/payment-service/main.go:682-694`). An attacker can spoof payment events.
- **Database TLS disabled** All services build `sslmode=disable` into the connection string with no override (`auth-service/main.go:126-147`, `work-management-service/main.go:91-114`, `payment-service/main.go:121-144`). Production connections would be unencrypted and unauthenticated.
## High Findings
- **JWT claim mismatch / brittle RBAC** Auth service issues tokens with `userId` while other services expect `user_id` (`auth-service/main.go:590-612` vs `work-management-service/main.go:172-178`, `payment-service/main.go:201-207`). Context user IDs become `nil`, and RBAC trusts whatever roles are in the token (already user-controlled). Inconsistent claims increase the chance of bypasses/bugs.
- **Replayable blockchain login** `verifyEthereumSignature` trusts arbitrary `message` provided by the client and stores no nonce/timestamp (`auth-service/main.go:241-320`). A captured signature can be replayed indefinitely to log in.
- **Over-permissive CORS** All services send `Access-Control-Allow-Origin: *` with broad methods/headers (`auth-service/main.go:151-164`, `work-management-service/main.go:116-130`, `payment-service/main.go:146-159`). In production this enables credential theft if tokens are ever stored/sent in browsers across origins.
- **Default secrets in compose** `podman-compose.yml` ships `your_super_secret_jwt_key` and `DB_PASSWORD=password` baked into service env, suitable only for dev but likely to be deployed as-is; no backend `.env.example` enumerating required secrets.
## Medium Findings
- **Payment/auth amounts handled as floats** API structs use `float64` for money (`payment-service/main.go:21-68`); conversions to cents rely on floating arithmetic (`main.go:641-665`). Risk of rounding errors; prefer fixed-point integers.
- **Task/status updates trust clients fully** No server-side validation of allowed status transitions or ownership (e.g., anyone can mark tasks completed or change invoice status).
- **Container hardening** Final images run as root (`FROM scratch`) with no `USER` directive and no CA certificates; TLS outbound would fail, and root runtime expands attack surface.
- **CI image builds may fail** Workflows call `podman build` on GitHub-hosted runners without ensuring podman availability/privileged mode; not a security bug but a reliability gap.
## Low Findings
- **Frontend XSS vector via Markdown** `ServiceDetailView.vue` and `ArticleDetailView.vue` render Markdown to HTML and inject via `v-html` without sanitization (`frontend/src/views/ServiceDetailView.vue:8-18`, `ArticleDetailView.vue:8-22`). Safe only if content is fully trusted and static; otherwise add sanitization.
- **JWT secret length not enforced** Auth service only checks non-empty (`auth-service/main.go:93-99`); mandate 3264+ random bytes.
- **No rate limiting / brute-force protection** Login endpoints have no throttling or lockouts.
---
## Database/Migrations Review
- Migrations are paired and reversible (001003 up/down present). Types and constraints look sane (ENUMs for status/roles, FK indexes present). No plaintext secrets stored.
- Missing: encryption requirements for sensitive columns (if any added later), and no migrations enforce audited timestamp updates for all tables beyond current set.
---
## Environment & Compliance
- **Missing backend `.env.example`**; secrets and required vars are not documented.
- `.gitignore` properly excludes `.env*`. No evidence of secrets committed.
- No HTTPS/TLS enforcement discussed for services; production should terminate TLS and set HSTS.
- Accessibility, GDPR, PCI: not evaluated in depth; Stripe used for payments (good), but webhook is insecure.
---
## Recommendations / Next Steps
1) **Fix auth/RBAC**: Remove client-controlled roles from registration; default to least-privileged and add an admin-only path to elevate. Align JWT claims (`userId` vs `user_id`) and validate roles server-side per endpoint. Add ownership checks for projects/tasks/invoices/payments.
2) **Secure transport & secrets**: Make `sslmode` configurable with `require`/`verify-full` for production; document vars in a backend `.env.example`; replace compose defaults with `${VAR}` placeholders.
3) **Stripe webhook**: Verify signatures (`Stripe-Signature`), parse events, and update payment status safely.
4) **CORS & HTTP hardening**: Restrict origins/methods/headers in prod; add request timeouts and rate limiting on auth endpoints.
5) **Blockchain login**: Require server-issued nonce/timestamp, bind signatures to that nonce, and expire after single use.
6) **Frontend sanitization**: Sanitize rendered Markdown (e.g., DOMPurify) or ensure content is immutable/trusted.
7) **Container/CI**: Add non-root `USER`, CA certificates, and health probes; ensure CI runners support podman or switch to docker/buildx.
8) **Monetary precision**: Store/accept currency values as integers of smallest unit; validate currency codes and ranges.
9) **Automated scans** (run outside this sandbox): `npm audit`/`npm audit --production`, `go mod verify`, `go vet`, `govulncheck`, `golangci-lint`, `trivy image`, and full test suites.
---
## Testing Performed
- Not run (sandboxed, offline). All findings from static analysis of repository files only.