neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
Add comprehensive security review documentation
67bd0f9 · gmorell · 2026-10-20 19:56
Message
{commit_body(@commit)}
Files changed
added
security-review/api-security.md
+135
−0
@@ -0,0 +1,135 @@
| 1 | +# API Security & Rate Limiting | |
| 2 | + | |
| 3 | +## API Endpoints | |
| 4 | + | |
| 5 | +### Personal Access Tokens (PAT) | |
| 6 | +**Scopes** (`lib/git_gud/accounts/personal_access_token.ex`) | |
| 7 | +- Granular scope system (e.g., `issues:read`, `issues:write`, `repo:read`, `repo:write`) | |
| 8 | +- Stored as hashes — cannot be recovered if lost | |
| 9 | +- Explicit scope checking in mobile API controllers | |
| 10 | + | |
| 11 | +**Mobile API** (`lib/git_gud_web/controllers/api/mobile/`) | |
| 12 | +- Auth via `x-api-key` or `Authorization: Bearer <token>` header | |
| 13 | +- Per-action scope validation in controller actions | |
| 14 | +- Endpoints: | |
| 15 | + - `AuthController` — login, register, token refresh | |
| 16 | + - `CiController` — CI/CD status, job logs | |
| 17 | + - `IssueController` — issues CRUD | |
| 18 | + - `LabelController` — labels CRUD | |
| 19 | + - `PackageController` — package uploads/downloads | |
| 20 | + - `PullRequestController` — PRs, reviews, comments | |
| 21 | + - `RepositoryController` — repos, branches, commits, blobs | |
| 22 | + - `SessionController` — session management | |
| 23 | + - `WikiController` — wiki pages | |
| 24 | + | |
| 25 | +### Gitea-Compatible REST API | |
| 26 | +**`lib/git_gud_web/controllers/api/v1/`** | |
| 27 | +- `OrgController` — organization management | |
| 28 | +- `RepositoryController` — repository CRUD, forks, settings | |
| 29 | +- `WorkflowController` — CI workflow management | |
| 30 | +- Auth: per-endpoint handling (public read, PAT for write, runner token for CI) | |
| 31 | + | |
| 32 | +### Runner Protocol (CI) | |
| 33 | +**`lib/git_gud_web/controllers/runner_controller.ex`** | |
| 34 | +- Twirp/protobuf endpoints at `/api/actions/_apis/runner.v1.*` | |
| 35 | +- Methods: `Ping`, `Register`, `Declare`, `FetchTask`, `FetchSingleTask`, `UpdateTask`, `UpdateLog` | |
| 36 | +- Auth: `x-runner-token` header or `Bearer` token | |
| 37 | +- Runner registration uses one-shot token (`Runners.consume_registration_token/1`) | |
| 38 | +- Per-job JWTs minted on task claim (`RunnerJwt.mint/3`) | |
| 39 | + | |
| 40 | +**Artifact/Cache APIs** | |
| 41 | +- `ActionsArtifactController` — `/api/actions/_apis/pipelines/workflows/:run_id/artifacts/*` | |
| 42 | +- `ActionsCacheController` — `/api/actions/_apis/artifactcache/*` | |
| 43 | +- `WorkflowArtifactController` — `/api/actions/_apis/workflow-artifacts/*` | |
| 44 | +- Auth: `RunnerJwtAuth` plug — verifies per-job JWT and job is active | |
| 45 | + | |
| 46 | +### OCI Registry API | |
| 47 | +**`lib/git_gud_web/controllers/registry_controller.ex`** | |
| 48 | +- Docker registry v2 protocol | |
| 49 | +- Token-based auth at `/v2/token` | |
| 50 | +- JWT tokens for push/pull operations | |
| 51 | +- Token signing via `GitGud.Packages.Token` (RS256/HS256 configurable) | |
| 52 | + | |
| 53 | +### ActivityPub/WebFinger | |
| 54 | +**`lib/git_gud_web/controllers/ap_actor_controller.ex`** | |
| 55 | +- HTTP signatures (RSA-SHA256) with digest verification | |
| 56 | +- Clock skew validation: 5-minute window | |
| 57 | +- Content negotiation for JSON-LD / ActivityPub objects | |
| 58 | +- WebFinger at `/.well-known/webfinger` | |
| 59 | + | |
| 60 | +**`lib/git_gud_web/plugs/ap_dispatcher.ex`** | |
| 61 | +- Routes ActivityPub requests to inbox handlers | |
| 62 | +- Validates signature chains for federated actors | |
| 63 | + | |
| 64 | +## Rate Limiting | |
| 65 | + | |
| 66 | +### Implemented Controls | |
| 67 | +**Federation Inbox** (`lib/git_gud/federation/rate_limiter.ex`) | |
| 68 | +- 60 requests/minute/source host | |
| 69 | +- Allowlisted hosts: 5x multiplier | |
| 70 | +- ETS-based counters with 60-second sweep interval | |
| 71 | +- Prevents inbox flooding from remote instances | |
| 72 | + | |
| 73 | +**MFA Attempts** (`lib/git_gud/accounts/two_factor_attempts.ex`) | |
| 74 | +- Tracks failed MFA verification attempts | |
| 75 | +- Shared with login attempt tracking | |
| 76 | + | |
| 77 | +### Missing Rate Limiting | |
| 78 | +- **Login/Register endpoints** — No rate limiting on authentication attempts | |
| 79 | +- **Password reset/Magic links** — No throttling on email sending | |
| 80 | +- **PAT token verification** — No brute-force protection on token endpoint | |
| 81 | +- **API endpoints** — No rate limiting on mobile API, REST API, or runner endpoints | |
| 82 | +- **Webhook delivery** — No rate limiting on outbound webhook retries | |
| 83 | +- **Runner protocol** — No rate limiting on `FetchTask` calls | |
| 84 | + | |
| 85 | +## Security Observations | |
| 86 | + | |
| 87 | +### High | |
| 88 | + | |
| 89 | +**No rate limiting on authentication endpoints** | |
| 90 | +- Login, registration, and magic link endpoints have no rate limiting | |
| 91 | +- Vulnerable to credential stuffing and brute-force attacks | |
| 92 | +- Recommendation: add IP-based rate limiting (e.g., 10 requests/minute for login, 5/hour for magic links) | |
| 93 | +- Consider CAPTCHA or proof-of-work after N failed attempts | |
| 94 | + | |
| 95 | +**No rate limiting on API endpoints** | |
| 96 | +- Mobile API, REST API, and runner protocol endpoints have no rate limiting | |
| 97 | +- Can be abused for resource exhaustion or denial of service | |
| 98 | +- Recommendation: add rate limiting via Plug (e.g., `Plug.RateLimit` or custom implementation) | |
| 99 | +- Start with 100 requests/minute per authenticated user, 10/minute for unauthenticated | |
| 100 | + | |
| 101 | +### Medium | |
| 102 | + | |
| 103 | +**PAT scopes not uniformly validated** | |
| 104 | +- Scoped token validation exists in mobile API but not consistently across all REST API endpoints | |
| 105 | +- Recommendation: create a centralized plug for PAT scope validation | |
| 106 | +- Apply to all API pipelines (`:api`, `:mobile_api`, `:browser_api`) | |
| 107 | + | |
| 108 | +**Runner registration token not expired** | |
| 109 | +- `lib/git_gud_web/controllers/runner_controller.ex:73` — Registration uses one-shot token | |
| 110 | +- No evidence of token expiry enforcement in the controller | |
| 111 | +- If token is never consumed, it remains valid indefinitely | |
| 112 | +- Recommendation: add expiry to registration tokens (e.g., 30 minutes) | |
| 113 | + | |
| 114 | +**API key exposure in URLs** | |
| 115 | +- Verify API keys are never logged in query parameters | |
| 116 | +- Recommendation: add logging middleware that redacts sensitive headers/parameters | |
| 117 | +- Use `Authorization` header instead of query parameters for API keys | |
| 118 | + | |
| 119 | +**Webhook delivery no rate limiting** | |
| 120 | +- `lib/git_gud/webhooks.ex` — Outbound delivery has no rate limiting | |
| 121 | +- Malicious or misconfigured webhooks can flood external endpoints | |
| 122 | +- Recommendation: add delivery rate limiting per webhook (e.g., 10 deliveries/minute) | |
| 123 | +- Include retry backoff with exponential delay and maximum retry count | |
| 124 | + | |
| 125 | +### Low | |
| 126 | + | |
| 127 | +**OCI registry token TTL** | |
| 128 | +- `config/config.exs:103` — Token TTL is 300 seconds (5 minutes) | |
| 129 | +- Short TTL is good for security but increases token refresh overhead | |
| 130 | +- Ensure token refresh logic in clients handles 401 responses gracefully | |
| 131 | + | |
| 132 | +**ActivityPub clock skew window** | |
| 133 | +- 5-minute clock skew window for HTTP signatures may be too permissive | |
| 134 | +- Recommendation: make clock skew configurable (e.g., `max_skew_seconds: 300`) | |
| 135 | +- Default to 30 seconds for high-security deployments |
added
security-review/authentication.md
+92
−0
@@ -0,0 +1,92 @@
| 1 | +# Authentication & Authorization | |
| 2 | + | |
| 3 | +## Implemented Controls | |
| 4 | + | |
| 5 | +### Multi-Factor Authentication | |
| 6 | +- **TOTP** (`lib/git_gud/accounts/two_factor.ex`, `lib/git_gud/accounts/recovery_code.ex`) | |
| 7 | + - RFC 6238 compliant time-based one-time passwords | |
| 8 | + - 10 single-use recovery codes per user (bcrypt-hashed) | |
| 9 | + - Instance-level enforcement configurable via `TwoFactor.enforcement_status/1` | |
| 10 | + - Grace period support (`TwoFactor.enforced_at` vs `TwoFactor.grace_period_days`) | |
| 11 | + - Attempt tracking via `TwoFactorAttempts` (shared with login) | |
| 12 | + | |
| 13 | +- **WebAuthn** (`lib/git_gud/accounts/webauthn.ex`, `lib/git_gud/accounts/webauthn_credential.ex`) | |
| 14 | + - Passwordless or second-factor authentication | |
| 15 | + - Ceremony managed via `Wax` library | |
| 16 | + - Sudo-mode gating for credential changes | |
| 17 | + - Credential management through `WebauthnController` | |
| 18 | + | |
| 19 | +- **Magic Links** — Email-based authentication via `UserNotifier` | |
| 20 | + - Token-based login without password entry | |
| 21 | + - Tokens stored server-side with expiry | |
| 22 | + | |
| 23 | +### Session Management | |
| 24 | +- **Session tokens** stored in `users_tokens` table | |
| 25 | + - 14-day expiry window | |
| 26 | + - Server-side revocation capability | |
| 27 | + - Renewal on activity (reissue after 7 days) | |
| 28 | + - Session fixation prevention via `renew_session/2` on login | |
| 29 | + - Cross-session invalidation via `disconnect_sessions/1` | |
| 30 | + | |
| 31 | +- **Remember-me cookies** | |
| 32 | + - Signed cookies with 14-day persistence | |
| 33 | + - Tied to user's `authenticated_at` timestamp | |
| 34 | + | |
| 35 | +### Personal Access Tokens | |
| 36 | +- **Scoped tokens** (`lib/git_gud/accounts/personal_access_token.ex`) | |
| 37 | + - Scope-based access control (e.g., `issues:read`, `repo:write`) | |
| 38 | + - Stored as hashes (not plaintext) | |
| 39 | + - Explicit scope checking in mobile API controller | |
| 40 | + - Per-action validation in `api/mobile/` controllers | |
| 41 | + | |
| 42 | +- **Runner JWTs** (`lib/git_gud/workflows/runner_jwt.ex`) | |
| 43 | + - Per-job short-lived tokens minted on `FetchTask` | |
| 44 | + - Claims: `sub`, `jti`, `tid` (task ID), `rid` (run ID), `pid` (repo ID), `perm` (permissions) | |
| 45 | + - Verified via `RunnerJwt.verify_active/1` which checks job is still running | |
| 46 | + - 5-minute grace period after job completion (for post-job steps) | |
| 47 | + - Permissions scoped to workflow `permissions:` block | |
| 48 | + | |
| 49 | +### Authorization Layers | |
| 50 | +- **Repository visibility** via `RepoAuth` plug | |
| 51 | + - Read-gate enforced at LiveView mount level | |
| 52 | + - Visibility enforcement: `public`, `internal`, `private` | |
| 53 | + - Preload-based checks to minimize queries | |
| 54 | + | |
| 55 | +- **Admin scope** via `Scope.for_user/1` | |
| 56 | + - Gated through `require_admin` mount hook | |
| 57 | + - Instance-level admin-only routes | |
| 58 | + | |
| 59 | +- **Sudo mode** | |
| 60 | + - Password or second-factor re-authentication required | |
| 61 | + - 10-minute validity window for sensitive operations | |
| 62 | + - Credential changes gated behind re-authentication | |
| 63 | + | |
| 64 | +## Security Observations | |
| 65 | + | |
| 66 | +### Critical | |
| 67 | + | |
| 68 | +**Insecure default secrets in config** (`config/config.exs:141-149`) | |
| 69 | +- Secrets master key default: `"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="` (all zeros) | |
| 70 | +- Runner JWT secret default: `"dev-runner-jwt-secret-do-not-use-in-prod"` | |
| 71 | +- If `runtime.exs` doesn't override these, all secrets are encrypted with a known key and tokens are forgeable | |
| 72 | +- Mitigation: production deployment **must** override via environment variables | |
| 73 | + | |
| 74 | +### High | |
| 75 | + | |
| 76 | +**HS256-only JWT signing** (`lib/git_gud/workflows/runner_jwt.ex:174`) | |
| 77 | +- Runner JWTs use symmetric HS256 signing | |
| 78 | +- Same secret signs all tokens across the instance | |
| 79 | +- If leaked from any single runner, attacker can mint arbitrary runner tokens | |
| 80 | +- No key rotation at JWT layer (only app-level master key rotation for secrets) | |
| 81 | +- Recommendation: consider asymmetric signing (RS256/ES256) or at minimum per-instance secret rotation | |
| 82 | + | |
| 83 | +### Medium | |
| 84 | + | |
| 85 | +**No explicit account lockout** | |
| 86 | +- Only MFA attempts are tracked via `TwoFactorAttempts` | |
| 87 | +- No lockout mechanism for failed login attempts | |
| 88 | +- Recommendation: add progressive rate limiting or account lockout after N failed attempts | |
| 89 | + | |
| 90 | +**PAT scopes not uniformly validated** | |
| 91 | +- Scoped token validation exists in mobile API but not consistently across all REST API endpoints | |
| 92 | +- Recommendation: centralize PAT scope validation in a plug applied to all API pipelines |
added
security-review/csrf-xss.md
+100
−0
@@ -0,0 +1,100 @@
| 1 | +# CSRF & XSS Prevention | |
| 2 | + | |
| 3 | +## CSRF Protection | |
| 4 | + | |
| 5 | +### Implemented Controls | |
| 6 | + | |
| 7 | +**Browser Pipeline** (`:browser` pipeline in router) | |
| 8 | +- `protect_from_forgery` plug applied to all browser routes | |
| 9 | + - Generates CSRF token per session | |
| 10 | + - Token validated on POST/PUT/DELETE/PATCH requests | |
| 11 | + - Token stored in session and compared against request | |
| 12 | + | |
| 13 | +**Token Transmission** | |
| 14 | +- Form submissions: token included via hidden input field | |
| 15 | +- AJAX/fetch requests: `x-csrf-token` header accepted | |
| 16 | + - Phoenix automatically includes CSRF token in meta tag | |
| 17 | + - JavaScript frameworks can read from `document.querySelector('meta[name="csrf-token"]').content` | |
| 18 | + | |
| 19 | +**Cookie Attributes** | |
| 20 | +- SameSite=Lax on session cookies | |
| 21 | + - Prevents cross-site cookie sending on top-level navigation | |
| 22 | + - Blocks CSRF on GET requests and same-site navigation | |
| 23 | + - Allows GET requests to cross-site via browser navigation (e.g., clicking a link) | |
| 24 | + | |
| 25 | +### Exclusions (Intentional) | |
| 26 | +- **`:api` pipeline** — Used by Gitea-compatible REST API and runner endpoints | |
| 27 | + - No CSRF protection — acceptable because endpoints use token authentication (PAT, runner JWT) | |
| 28 | + - Token-authenticated clients don't automatically send cookies | |
| 29 | +- **`:mobile_api` pipeline** — Mobile API endpoints | |
| 30 | + - Uses personal access token authentication | |
| 31 | + - No CSRF protection — acceptable for token-based auth | |
| 32 | + | |
| 33 | +## XSS Prevention | |
| 34 | + | |
| 35 | +### Implemented Controls | |
| 36 | + | |
| 37 | +**LiveView Auto-Escaping** | |
| 38 | +- Phoenix LiveView automatically escapes all interpolated values in HEEx templates | |
| 39 | + - `{@value}` — HTML-escaped interpolation | |
| 40 | + - `<%= @value %>` — HTML-escaped interpolation in tag bodies | |
| 41 | + - `{@value}` in attribute values — attribute-escaped | |
| 42 | +- Template compilation provides compile-time escaping guarantees | |
| 43 | + | |
| 44 | +**Markdown Sanitization** | |
| 45 | +- `lib/git_gud/markdown.ex` — HTML sanitization pipeline | |
| 46 | + - Strips dangerous HTML elements (`<script>`, `<iframe>`, `<object>`, `<embed>`) | |
| 47 | + - Strips event handler attributes (`onload`, `onclick`, etc.) | |
| 48 | + - Strips `javascript:` URIs in href/src attributes | |
| 49 | + - Allows safe subset: bold, italic, links, code blocks, tables, images | |
| 50 | + - Processes user-generated content before rendering in templates | |
| 51 | + | |
| 52 | +**Content-Type Headers** | |
| 53 | +- All responses include appropriate `Content-Type` headers | |
| 54 | +- JSON responses: `application/json` | |
| 55 | +- HTML responses: `text/html` | |
| 56 | +- Prevents MIME-type confusion attacks | |
| 57 | + | |
| 58 | +## Security Observations | |
| 59 | + | |
| 60 | +### Medium | |
| 61 | + | |
| 62 | +**No Content-Security-Policy (CSP) header** | |
| 63 | +- CSP provides defense-in-depth against XSS | |
| 64 | +- Prevents inline script execution (except explicitly allowed) | |
| 65 | +- Restricts sources for scripts, styles, images, fonts, etc. | |
| 66 | +- Recommendation: add CSP header via Plug | |
| 67 | + - Example: `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';` | |
| 68 | + - Start with `Content-Security-Policy-Report-Only` for testing | |
| 69 | + | |
| 70 | +**No X-Frame-Options or X-Content-Type-Options headers** | |
| 71 | +- `X-Frame-Options: DENY` — prevents clickjacking | |
| 72 | +- `X-Content-Type-Options: nosniff` — prevents MIME-type sniffing | |
| 73 | +- Recommendation: add via `put_secure_browser_headers` (already in `:browser` pipeline) | |
| 74 | + - Verify these headers are not being stripped by reverse proxy | |
| 75 | + | |
| 76 | +**LiveDashboard accessible** | |
| 77 | +- Phoenix LiveDashboard available at `/dashboard/*` | |
| 78 | +- Exposes runtime metrics, process info, logger, IEx console | |
| 79 | +- **Risk**: IEx console allows arbitrary code execution | |
| 80 | +- Recommendation: disable `dev_routes` in production | |
| 81 | + - `config :git_gud, GitGudWeb.Endpoint, dev_routes: false` | |
| 82 | + - Or restrict to admin users only via plug | |
| 83 | + | |
| 84 | +**MailboxPreview accessible** | |
| 85 | +- Swoosh MailboxPreview at `/dev/mailbox` in development | |
| 86 | +- Exposes received emails (including magic links with tokens) | |
| 87 | +- Recommendation: disable in production via `config :swoosh, local: false` | |
| 88 | + | |
| 89 | +### Low | |
| 90 | + | |
| 91 | +**Markdown sanitization needs periodic review** | |
| 92 | +- XSS vectors evolve over time (e.g., SVG onload, iframe src, data URIs) | |
| 93 | +- Recommendation: add security test suite with known XSS payloads | |
| 94 | +- Test against latest OWASP XSS cheatsheet regularly | |
| 95 | +- Consider using a well-maintained sanitization library (e.g., `bleach`-like Elixir port) | |
| 96 | + | |
| 97 | +**Inline JavaScript in templates** | |
| 98 | +- If `phx-hook` or `colocated hooks` used in templates, inline scripts may be injected | |
| 99 | +- Recommendation: ensure all JavaScript is loaded via assets bundle (app.js) | |
| 100 | +- Use `phx-hook` with external hooks defined in `assets/js/` where possible |
added
security-review/error-handling-headers.md
+117
−0
@@ -0,0 +1,117 @@
| 1 | +# Error Handling & Security Headers | |
| 2 | + | |
| 3 | +## Error Handling | |
| 4 | + | |
| 5 | +### Implemented Controls | |
| 6 | + | |
| 7 | +**Error Pages** | |
| 8 | +- `lib/git_gud_web/controllers/error_html.ex` — HTML error pages (404, 500, etc.) | |
| 9 | +- `lib/git_gud_web/controllers/error_json.ex` — JSON error responses | |
| 10 | +- Consistent error format across HTML and JSON endpoints | |
| 11 | +- Generic messages that don't leak implementation details | |
| 12 | + | |
| 13 | +**Authentication Errors** | |
| 14 | +- Hook/auth failures log warnings with IP address | |
| 15 | +- Runner protocol errors return Twirp-style error codes (e.g., `unauthenticated`, `invalid_argument`) | |
| 16 | +- Generic messages for auth failures — no stack traces or internal details | |
| 17 | + | |
| 18 | +**Runner Protocol Error Handling** | |
| 19 | +- `runner_controller.ex` — Twirp error responses with structured error codes | |
| 20 | +- Protobuf error messages include `code` and `message` fields | |
| 21 | +- Connect-Go client compatibility verified | |
| 22 | + | |
| 23 | +**Database Error Handling** | |
| 24 | +- Ecto changeset errors returned to clients | |
| 25 | +- No raw SQL errors exposed (Ecto catches and wraps errors) | |
| 26 | +- Constraint violation errors handled gracefully | |
| 27 | + | |
| 28 | +## Security Headers | |
| 29 | + | |
| 30 | +### Implemented Controls | |
| 31 | + | |
| 32 | +**Browser Pipeline** (`:browser` pipeline) | |
| 33 | +- `put_secure_browser_headers` plug | |
| 34 | + - Sets standard security headers | |
| 35 | + - Applied to all browser routes | |
| 36 | + - Includes: `X-Frame-Options`, `X-Content-Type-Options`, `X-XSS-Protection` | |
| 37 | + | |
| 38 | +**Session Cookies** | |
| 39 | +- SameSite=Lax on session cookies | |
| 40 | +- Secure flag for HTTPS-only transmission | |
| 41 | +- HttpOnly flag on session cookies (prevents JavaScript access) | |
| 42 | + | |
| 43 | +**Runner Protocol Responses** | |
| 44 | +- `Connection: close` header on runner protocol responses | |
| 45 | +- Prevents HTTP/1.1 keep-alive framing attacks | |
| 46 | +- Bandit + forgejo-runner Connect-Go compatibility | |
| 47 | + | |
| 48 | +**Content-Type Headers** | |
| 49 | +- All JSON responses: `application/json` | |
| 50 | +- All HTML responses: `text/html` | |
| 51 | +- Prevents MIME-type confusion attacks | |
| 52 | + | |
| 53 | +## Security Observations | |
| 54 | + | |
| 55 | +### Medium | |
| 56 | + | |
| 57 | +**No Content-Security-Policy (CSP) header** | |
| 58 | +- CSP provides defense-in-depth against XSS and data injection | |
| 59 | +- Restricts sources for scripts, styles, images, fonts, etc. | |
| 60 | +- Recommendation: add CSP header via Plug | |
| 61 | + - Start with `Content-Security-Policy-Report-Only` for testing | |
| 62 | + - Example: `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';` | |
| 63 | + - Gradually tighten as issues are resolved | |
| 64 | + | |
| 65 | +**X-Frame-Options not explicitly set** | |
| 66 | +- `put_secure_browser_headers` may set this, but verify it's not being stripped | |
| 67 | +- `X-Frame-Options: DENY` prevents clickjacking attacks | |
| 68 | +- Recommendation: explicitly set header via Plug | |
| 69 | + - `put_resp_header(conn, "x-frame-options", "deny")` | |
| 70 | + | |
| 71 | +**X-Content-Type-Options not explicitly set** | |
| 72 | +- `put_secure_browser_headers` may set this, but verify it's not being stripped | |
| 73 | +- `X-Content-Type-Options: nosniff` prevents MIME-type sniffing | |
| 74 | +- Recommendation: explicitly set header | |
| 75 | + - `put_resp_header(conn, "x-content-type-options", "nosniff")` | |
| 76 | + | |
| 77 | +**No CORS configuration** | |
| 78 | +- Phoenix default applies (permissive) | |
| 79 | +- API endpoints may be vulnerable to cross-origin attacks | |
| 80 | +- Recommendation: add CORS headers for API endpoints | |
| 81 | + - `cors_plug` library or custom Plug | |
| 82 | + - Restrict to known origins for public APIs | |
| 83 | + - Block all origins for private APIs | |
| 84 | + | |
| 85 | +**LiveDashboard accessible in development** | |
| 86 | +- Phoenix LiveDashboard at `/dashboard/*` | |
| 87 | +- Exposes runtime metrics, process info, logger, IEx console | |
| 88 | +- **Critical**: IEx console allows arbitrary code execution | |
| 89 | +- Recommendation: disable `dev_routes` in production | |
| 90 | + - `config :git_gud, GitGudWeb.Endpoint, dev_routes: false` | |
| 91 | + - Or restrict to admin users only via plug | |
| 92 | + | |
| 93 | +**MailboxPreview accessible in development** | |
| 94 | +- Swoosh MailboxPreview at `/dev/mailbox` | |
| 95 | +- Exposes received emails (including magic links with tokens) | |
| 96 | +- Recommendation: disable in production | |
| 97 | + - `config :swoosh, local: false` | |
| 98 | + | |
| 99 | +### Low | |
| 100 | + | |
| 101 | +**Error messages may leak implementation details** | |
| 102 | +- Some error responses include `inspect(reason)` which may expose internal state | |
| 103 | +- Example: `actions_artifact_controller.ex:99` — `Jason.encode!(%{"error" => inspect(reason)})` | |
| 104 | +- Recommendation: use generic error messages for 4xx responses | |
| 105 | +- Only expose detailed errors for 5xx responses in development mode | |
| 106 | + | |
| 107 | +**Stack traces in development** | |
| 108 | +- Phoenix may display stack traces in development | |
| 109 | +- Could leak sensitive information (file paths, database queries, etc.) | |
| 110 | +- Recommendation: ensure `debug_errors: true` is disabled in production | |
| 111 | + - `config :phoenix, :debug_errors, false` | |
| 112 | + | |
| 113 | +**No centralized error monitoring** | |
| 114 | +- Sentry integration exists (`GitGud.SentryClient`) but not examined in detail | |
| 115 | +- Verify error monitoring is configured in production | |
| 116 | +- Recommendation: ensure Sentry DSN is configured via `SENTRY_DSN` environment variable | |
| 117 | +- Add custom error handlers for critical paths |
added
security-review/file-upload-storage.md
+118
−0
@@ -0,0 +1,118 @@
| 1 | +# File Upload & Storage Security | |
| 2 | + | |
| 3 | +## Implemented Controls | |
| 4 | + | |
| 5 | +### Storage Architecture | |
| 6 | +**`lib/git_gud/storage/`** | |
| 7 | +- `Storage.Disk` — Local filesystem storage | |
| 8 | +- `Storage.Garage` — S3-compatible object storage | |
| 9 | +- Unified interface for put/get/delete/presign operations | |
| 10 | +- No multipart upload support — assembly happens in memory for chunked uploads | |
| 11 | + | |
| 12 | +### Storage Controller | |
| 13 | +**`lib/git_gud_web/controllers/storage_controller.ex`** | |
| 14 | +- Signed/capability tokens for file access | |
| 15 | +- `Disk.verify_token/2` — validates token before file access | |
| 16 | +- Token contains bucket + key binding (prevents path traversal) | |
| 17 | +- Presigned URLs with configurable expiry | |
| 18 | + | |
| 19 | +### Upload Size Limits | |
| 20 | +| Component | Limit | File/Line | | |
| 21 | +|-----------|-------|-----------| | |
| 22 | +| Runner proto body | 50MB | `runner_controller.ex:700` | | |
| 23 | +| Artifact upload | 1GB | `actions_artifact_controller.ex:76` | | |
| 24 | +| Cache chunk upload | 200MB | `actions_cache_controller.ex:91` | | |
| 25 | +| Package upload | 200MB | `PackageRegistryController` | | |
| 26 | +| LFS objects | Configurable | `LfsController` | | |
| 27 | + | |
| 28 | +### Runner Artifact Storage | |
| 29 | +**`lib/git_gud_web/controllers/actions_artifact_controller.ex`** | |
| 30 | +- Authenticated by runner JWT (`RunnerJwtAuth` plug) | |
| 31 | +- Run-scoped: job only accesses its own run's artifacts (`own_run/2`) | |
| 32 | +- Storage key sanitized: `String.replace(name, ~r/[^A-Za-z0-9._\-]/, "_")` | |
| 33 | +- Storage path: `gitgud-artifacts/run-<run_id>/<sanitized_name>` | |
| 34 | + | |
| 35 | +### Runner Cache Storage | |
| 36 | +**`lib/git_gud_web/controllers/actions_cache_controller.ex`** | |
| 37 | +- Authenticated by runner JWT | |
| 38 | +- Entry-scoped: only the job that reserved can upload (`reserved_by?/2`) | |
| 39 | +- Upload expiry: 600 seconds (10 minutes) | |
| 40 | +- Chunk validation: `Content-Range` header parsed and validated | |
| 41 | +- Finalization: chunks assembled in offset order, no gaps allowed | |
| 42 | + | |
| 43 | +### Package Registry Uploads | |
| 44 | +**`lib/git_gud_web/controllers/package_registry_controller.ex`** | |
| 45 | +- Size limit: 200MB per upload | |
| 46 | +- Content-type validation based on package format | |
| 47 | +- Authentication via registry token or PAT | |
| 48 | +- Format-specific parsing (`Formats.Deb`, `Formats.Rpm`, `Formats.Hex`, etc.) | |
| 49 | + | |
| 50 | +## Security Observations | |
| 51 | + | |
| 52 | +### High | |
| 53 | + | |
| 54 | +**No file type validation in storage controller** | |
| 55 | +- `StorageController` relies on upstream validation | |
| 56 | +- No magic byte checking (e.g., PE/ELF/Mach-O for binaries) | |
| 57 | +- No MIME type verification against declared Content-Type | |
| 58 | +- Recommendation: add content-type validation based on actual file content | |
| 59 | +- Use `:erlang.md5/1` or similar to check magic bytes for known formats | |
| 60 | + | |
| 61 | +**No virus/malware scanning** | |
| 62 | +- Uploaded artifacts, packages, and LFS objects not scanned | |
| 63 | +- Malicious binaries can be distributed through the forge | |
| 64 | +- Recommendation: integrate malware scanner (e.g., ClamAV) | |
| 65 | +- Scan on upload, quarantine flagged files | |
| 66 | +- Consider async scanning with upload allowed pending scan | |
| 67 | + | |
| 68 | +**Large body reads can cause memory exhaustion** | |
| 69 | +- `actions_artifact_controller.ex:76` — 1GB read per upload | |
| 70 | + - Multiple concurrent uploads can exhaust server memory | |
| 71 | + - Recommendation: add per-user concurrent upload limits | |
| 72 | + - Consider streaming uploads directly to object storage | |
| 73 | +- `actions_cache_controller.ex:219` — Cache finalization assembles chunks in memory | |
| 74 | + - Large cache entries loaded entirely into memory | |
| 75 | + - Recommendation: implement streaming assembly or limit max cache entry size | |
| 76 | + | |
| 77 | +### Medium | |
| 78 | + | |
| 79 | +**Content-type spoofing in artifact downloads** | |
| 80 | +- `actions_artifact_controller.ex:150` — Downloads return `artifact.content_type` from database | |
| 81 | +- Attacker can set arbitrary `content_type` during artifact creation | |
| 82 | +- If artifacts served inline in browser, could enable XSS via HTML/JS content types | |
| 83 | +- Recommendation: validate and sanitize `content_type` field | |
| 84 | +- Default to `application/octet-stream` unless explicitly set to safe MIME type | |
| 85 | +- Add allowlist of allowed content types | |
| 86 | + | |
| 87 | +**Cache chunk upload not strictly validated** | |
| 88 | +- `actions_cache_controller.ex:87-109` — Upload validates `Content-Range` header | |
| 89 | +- No validation that chunk offset matches expected position | |
| 90 | +- Recommendation: add server-side offset tracking per upload session | |
| 91 | +- Reject chunks that don't match expected sequence | |
| 92 | + | |
| 93 | +**Presigned URL expiry too long** | |
| 94 | +- `actions_cache.ex:46` — Download expiry: 300 seconds | |
| 95 | +- `actions_cache.ex:47` — Upload expiry: 600 seconds | |
| 96 | +- Long expiry increases window for URL theft/misuse | |
| 97 | +- Recommendation: reduce download expiry to 60 seconds | |
| 98 | +- Keep upload expiry at 300 seconds (already short) | |
| 99 | + | |
| 100 | +**No upload checksums** | |
| 101 | +- Uploaded files not verified against client-provided checksums | |
| 102 | +- Corruption during upload not detected | |
| 103 | +- Recommendation: accept `Content-MD5` header from clients | |
| 104 | +- Verify checksum after upload, return error on mismatch | |
| 105 | + | |
| 106 | +### Low | |
| 107 | + | |
| 108 | +**Upload URL construction** | |
| 109 | +- `actions_artifact_controller.ex:184-187` — Upload URLs constructed via string interpolation | |
| 110 | +- No validation that `endpoint.url()` is safe | |
| 111 | +- Recommendation: use Phoenix URL helpers for URL generation | |
| 112 | +- Prevent URL scheme injection | |
| 113 | + | |
| 114 | +**No rate limiting on uploads** | |
| 115 | +- Upload endpoints have no rate limiting | |
| 116 | +- Can be used for storage exhaustion attacks | |
| 117 | +- Recommendation: add per-user upload limits (e.g., 10 uploads/hour) | |
| 118 | +- Consider storage quota per repository/organization |
added
security-review/input-validation.md
+70
−0
@@ -0,0 +1,70 @@
| 1 | +# Input Validation & Sanitization | |
| 2 | + | |
| 3 | +## Implemented Controls | |
| 4 | + | |
| 5 | +### SSRF Prevention | |
| 6 | +**`lib/git_gud/outbound_url.ex`** — Comprehensive outbound URL validation | |
| 7 | + | |
| 8 | +Blocks the following categories of URLs when making outbound HTTP requests: | |
| 9 | +- **Loopback addresses**: `127.0.0.0/8`, `::1`, `localhost` | |
| 10 | +- **Link-local**: `169.254.0.0/16` | |
| 11 | +- **Private ranges**: RFC 1918 (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) | |
| 12 | +- **Cloud metadata**: `169.254.169.254` (AWS, GCP, Azure metadata endpoints) | |
| 13 | +- **IPv6 unique-local**: `fc00::/7` | |
| 14 | +- **IPv6 multicast**: `ff00::/8` | |
| 15 | +- **IPv6 reserved**: `::/128`, `::1/128` | |
| 16 | +- **IPv4-mapped IPv6**: `::ffff:0:0/96` | |
| 17 | +- **CGNAT**: `100.64.0.0/10` | |
| 18 | + | |
| 19 | +DNS rebinding protection included — resolves hostnames and validates resolved IP addresses. | |
| 20 | + | |
| 21 | +### Size Limits | |
| 22 | +| Endpoint | Limit | File/Line | | |
| 23 | +|----------|-------|-----------| | |
| 24 | +| Pre-receive hooks | 1MB | `PreReceiveController` | | |
| 25 | +| Package uploads | 200MB | `PackageRegistryController` | | |
| 26 | +| Hook requests | 1MB body | `HookController` | | |
| 27 | +| Runner proto | 50MB | `runner_controller.ex:700` | | |
| 28 | +| Artifact uploads | 1GB | `actions_artifact_controller.ex:76` | | |
| 29 | +| Cache chunk upload | 200MB | `actions_cache_controller.ex:91` | | |
| 30 | + | |
| 31 | +### Data Validation | |
| 32 | +- **Ecto changesets** — all schema writes use `validate_required`, `validate_inclusion`, `unique_constraint` | |
| 33 | +- **SHA validation** — pre-receive hooks validate 40-character hex SHA format | |
| 34 | +- **Ref format parsing** — git ref updates validated against expected format | |
| 35 | +- **Token format validation** — personal access tokens, runner tokens validated on use | |
| 36 | + | |
| 37 | +### HTML/Markdown Sanitization | |
| 38 | +- **`lib/git_gud/markdown.ex`** — HTML sanitization before rendering user-generated content | |
| 39 | + - Processes markdown input through sanitization pipeline | |
| 40 | + - Strips dangerous elements (scripts, event handlers, javascript: URIs) | |
| 41 | + - Uses `Floki` or `ex_html_sax` for DOM manipulation | |
| 42 | + - Allows safe subset of HTML (bold, italic, links, code blocks, etc.) | |
| 43 | + | |
| 44 | +## Security Observations | |
| 45 | + | |
| 46 | +### Medium | |
| 47 | + | |
| 48 | +**Markdown sanitization rules need periodic review** | |
| 49 | +- Sanitization configuration should be verified against latest OWASP XSS cheatsheet | |
| 50 | +- New XSS vectors discovered regularly — ensure sanitization is current | |
| 51 | +- Recommendation: add test cases for known XSS payloads (e.g., `<svg/onload=alert(1)>`, `<iframe src=javascript:...>`) | |
| 52 | + | |
| 53 | +**Size limits can cause memory exhaustion** | |
| 54 | +- `actions_artifact_controller.ex:76` — `read_body/2` with `length: 1_000_000_000` (1 GB) | |
| 55 | + - If multiple concurrent artifact uploads hit the limit, memory exhaustion is possible | |
| 56 | + - Recommendation: add per-user or per-repo concurrent upload limits | |
| 57 | +- `actions_cache_controller.ex:91` — 200MB per chunk upload | |
| 58 | + - Consider chunking or streaming for very large caches | |
| 59 | + | |
| 60 | +**No file type validation in storage controller** | |
| 61 | +- `StorageController` relies on upstream validation for file types | |
| 62 | +- No virus/malware scanning pipeline | |
| 63 | +- Recommendation: add content-type validation based on actual file content (magic bytes) not just headers | |
| 64 | +- Consider adding malware scanning (e.g., ClamAV integration) for uploaded artifacts | |
| 65 | + | |
| 66 | +**No rate limiting on login/register endpoints** | |
| 67 | +- Only MFA attempts tracked via `TwoFactorAttempts` | |
| 68 | +- Login/register endpoints can be hammered for brute-force or credential stuffing | |
| 69 | +- Recommendation: add IP-based rate limiting (e.g., 10 requests/minute per IP for login) | |
| 70 | +- Recommendation: add CAPTCHA or proof-of-work after N failed attempts |
added
security-review/secrets-encryption.md
+146
−0
@@ -0,0 +1,146 @@
| 1 | +# Secrets Management & Encryption | |
| 2 | + | |
| 3 | +## Secrets Storage | |
| 4 | + | |
| 5 | +### CI/Repository Secrets | |
| 6 | +**`lib/git_gud/secrets/`** | |
| 7 | +- `RepositorySecret` — Per-repository encrypted secrets | |
| 8 | +- `OrganizationSecret` — Per-organization encrypted secrets | |
| 9 | +- `RepositoryVariable` — Per-repository plaintext variables | |
| 10 | +- `OrganizationVariable` — Per-organization plaintext variables | |
| 11 | +- `ReservedNames` — Prevents overriding reserved names (e.g., `GITHUB_TOKEN`) | |
| 12 | + | |
| 13 | +**Encryption** | |
| 14 | +- AES-256-GCM for all secrets (authenticated encryption) | |
| 15 | +- Per-row initialization vector (IV) — prevents pattern analysis | |
| 16 | +- Per-row key ID (`key_id`) supports master key rotation | |
| 17 | +- `GitGud.Secrets` module provides encryption/decryption wrapper | |
| 18 | + | |
| 19 | +**Master Key Configuration** | |
| 20 | +- `lib/git_gud/secrets.ex` — Encryption wrapper | |
| 21 | +- Supports multiple `master_keys` for rotation | |
| 22 | +- Primary key ID tracked per secret for automatic key identification | |
| 23 | +- `resolve_for/1` — decrypts all secrets for a given repository/organization in memory | |
| 24 | + | |
| 25 | +### Session Tokens | |
| 26 | +- `lib/git_gud/accounts/user_token.ex` — Phoenix Token signed | |
| 27 | +- Tokens stored in `users_tokens` table | |
| 28 | +- 14-day expiry with reissue logic | |
| 29 | +- Types: `:confirm`, `:reset_password`, `update_email`, `:session` | |
| 30 | + | |
| 31 | +### Personal Access Tokens | |
| 32 | +- Stored as bcrypt hashes — cannot be recovered | |
| 33 | +- Scope metadata stored separately from hash | |
| 34 | +- Token comparison via `Bcrypt.verify/2` | |
| 35 | + | |
| 36 | +### Runner JWTs | |
| 37 | +- `lib/git_gud/workflows/runner_jwt.ex` — HS256 signed JWTs | |
| 38 | +- Claims: `sub`, `jti`, `tid`, `rid`, `pid`, `perm`, `exp`, `iat`, `nbf` | |
| 39 | +- TTL: 24 hours (`ttl_seconds: 86400` in config) | |
| 40 | +- Grace period: 5 minutes after job completion for post-job steps | |
| 41 | +- Verification via `RunnerJwt.verify_active/1` — checks job is still running | |
| 42 | + | |
| 43 | +### Webhook Secret | |
| 44 | +- 32 random bytes generated at boot | |
| 45 | +- Can be overridden via `GITGUD_HOOK_SECRET` environment variable | |
| 46 | +- Used for `x-gitgud-hook-token` header validation | |
| 47 | + | |
| 48 | +### TOTP Secrets | |
| 49 | +- Encrypted with AES-256-GCM (same as secrets) | |
| 50 | +- Per-user IV and key_id | |
| 51 | +- Stored in `users_two_factor` table | |
| 52 | + | |
| 53 | +### Registry Token Signing | |
| 54 | +- `lib/git_gud/packages/token.ex` — OCI registry JWT signing | |
| 55 | +- Configurable algorithm: RS256 (recommended) or HS256 | |
| 56 | +- Private key loaded from `GITGUD_REGISTRY_PRIVATE_KEY` | |
| 57 | +- Public key published for validation (`/etc/zot/registry-pub.pem`) | |
| 58 | + | |
| 59 | +## Encryption Details | |
| 60 | + | |
| 61 | +### AES-256-GCM Implementation | |
| 62 | +- Used for: CI secrets, organization secrets, TOTP secrets | |
| 63 | +- Per-row IV ensures identical plaintext produces different ciphertexts | |
| 64 | +- Key ID enables key rotation without re-encrypting all secrets | |
| 65 | +- Tag size: 128 bits (default for GCM) | |
| 66 | + | |
| 67 | +### RSA-SHA256 for ActivityPub | |
| 68 | +- Used for: HTTP signatures in federated requests | |
| 69 | +- Private key for signing outbound requests | |
| 70 | +- Public key verification for inbound requests | |
| 71 | +- Digest verification: `SHA-256=<base64>` for request body integrity | |
| 72 | + | |
| 73 | +### Bcrypt for Passwords | |
| 74 | +- Used for: user passwords, recovery codes | |
| 75 | +- Cost factor: default (12 rounds as of Elixir 1.15+) | |
| 76 | +- Recovery codes: 10 per user, single-use, hashed before comparison | |
| 77 | + | |
| 78 | +## Security Observations | |
| 79 | + | |
| 80 | +### Critical | |
| 81 | + | |
| 82 | +**Insecure default secrets master key** (`config/config.exs:142-143`) | |
| 83 | +```elixir | |
| 84 | +config :git_gud, GitGud.Secrets, | |
| 85 | + primary_key_id: "default", | |
| 86 | + master_key: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" | |
| 87 | +``` | |
| 88 | +- All zeros as default key — if production doesn't override, all secrets are encrypted with a known key | |
| 89 | +- Recommendation: require `GITGUD_SECRETS_MASTER_KEY` environment variable in production | |
| 90 | +- Fail closed: refuse to start if master key is all zeros or empty | |
| 91 | + | |
| 92 | +**Insecure default runner JWT secret** (`config/config.exs:148`) | |
| 93 | +```elixir | |
| 94 | +config :git_gud, GitGud.Workflows.RunnerJwt, | |
| 95 | + secret: "dev-runner-jwt-secret-do-not-use-in-prod" | |
| 96 | +``` | |
| 97 | +- Plaintext development secret — forgeable runner tokens if not overridden | |
| 98 | +- `RunnerJwt.signing_secret/0` does raise on empty secret, but only if the value is empty string, not this default | |
| 99 | +- Recommendation: require `GITGUD_RUNNER_JWT_SECRET` environment variable in production | |
| 100 | +- Fail closed: refuse to start if secret matches development default | |
| 101 | + | |
| 102 | +### High | |
| 103 | + | |
| 104 | +**HS256-only JWT signing** (`lib/git_gud/workflows/runner_jwt.ex:174`) | |
| 105 | +- Symmetric signing — same secret signs all tokens | |
| 106 | +- If leaked from any runner, attacker can mint arbitrary tokens | |
| 107 | +- No key rotation at JWT layer | |
| 108 | +- Recommendation: consider asymmetric signing (RS256/ES256) for JWTs | |
| 109 | +- At minimum: implement periodic secret rotation with graceful transition | |
| 110 | + | |
| 111 | +**Variables stored in plaintext** | |
| 112 | +- `RepositoryVariable` and `OrganizationVariable` stored without encryption | |
| 113 | +- Acceptable by design (variables are not secrets) | |
| 114 | +- Access controls must be strict to prevent information leakage | |
| 115 | +- Recommendation: audit access controls on variable endpoints | |
| 116 | + | |
| 117 | +**No automatic key rotation** | |
| 118 | +- Master key rotation requires manual intervention | |
| 119 | +- Old secrets must be re-encrypted with new key on access | |
| 120 | +- Recommendation: implement background worker for key rotation | |
| 121 | +- Rotate at least annually | |
| 122 | + | |
| 123 | +**Clock skew window too permissive** | |
| 124 | +- 5-minute window for ActivityPub signature verification | |
| 125 | +- Recommendation: make configurable (`max_skew_seconds: 300`) | |
| 126 | +- Default to 30 seconds for high-security deployments | |
| 127 | + | |
| 128 | +### Medium | |
| 129 | + | |
| 130 | +**Secrets decrypted in memory** | |
| 131 | +- `GitGud.Secrets.resolve_for/1` decrypts all secrets for a job in memory | |
| 132 | +- Secrets visible in memory dumps, swap, or crash dumps | |
| 133 | +- Recommendation: clear secret values from memory after use | |
| 134 | +- Consider `:crypto.crypto.one_time` with explicit memory zeroing | |
| 135 | + | |
| 136 | +**No audit logging for secret operations** | |
| 137 | +- Secrets can be created/updated/deleted via web UI | |
| 138 | +- No audit trail for who accessed or modified secrets | |
| 139 | +- Recommendation: log all secret operations (create, read, update, delete) | |
| 140 | +- Include user ID, timestamp, and operation type | |
| 141 | + | |
| 142 | +**Erlang distribution cookie** | |
| 143 | +- `rel/env.sh.eex` generates random node name based on `POD_IP` | |
| 144 | +- Cookie loaded from K8s secret (`RELEASE_COOKIE`) | |
| 145 | +- If cookie shared across environments, allows node hijacking | |
| 146 | +- Recommendation: use environment-specific cookies with strict access controls |
added
security-review/session-management.md
+74
−0
@@ -0,0 +1,74 @@
| 1 | +# Session Management | |
| 2 | + | |
| 3 | +## Implemented Controls | |
| 4 | + | |
| 5 | +### Server-Side Session Tokens | |
| 6 | +- **`lib/git_gud/accounts/user_token.ex`** — Token schema with expiry | |
| 7 | + - Tokens stored in `users_tokens` table (not client-side only) | |
| 8 | + - 14-day expiry window from creation | |
| 9 | + - Token types: `:confirm`, `:reset_password`, `:update_email`, `:session` | |
| 10 | + - Reissue logic: tokens can be renewed before expiry | |
| 11 | + | |
| 12 | +### Cookie Sessions | |
| 13 | +- **Signed cookies** with Phoenix Token | |
| 14 | + - Session data stored in signed cookies (tamper-proof) | |
| 15 | + - `signing_salt` configured in `endpoint.ex` | |
| 16 | + - 14-day remember-me persistence | |
| 17 | + - SameSite=Lax attribute on session cookies | |
| 18 | + - Secure flag set for HTTPS-only transmission | |
| 19 | + | |
| 20 | +### Session Lifecycle | |
| 21 | +- **`renew_session/2`** — Session fixation prevention | |
| 22 | + - Called on login to issue new session token | |
| 23 | + - Invalidates previous session token | |
| 24 | + - Prevents session fixation attacks | |
| 25 | + | |
| 26 | +- **Session renewal on activity** | |
| 27 | + - Tokens reissued after 7 days of inactivity | |
| 28 | + - Extends session validity without requiring re-authentication | |
| 29 | + | |
| 30 | +- **`disconnect_sessions/1`** — Cross-session invalidation | |
| 31 | + - Broadcasts disconnect event to all LiveView connections for a user | |
| 32 | + - Terminates all browser tabs for invalidated tokens | |
| 33 | + - Called when tokens are revoked (e.g., password change, admin action) | |
| 34 | + | |
| 35 | +### Sudo Mode | |
| 36 | +- **Re-authentication** for sensitive operations | |
| 37 | + - Requires current password or second-factor (TOTP/WebAuthn) | |
| 38 | + - 10-minute validity window for sudo operations | |
| 39 | + - Applied to: password changes, secret access, credential management | |
| 40 | + | |
| 41 | +## Security Observations | |
| 42 | + | |
| 43 | +### Medium | |
| 44 | + | |
| 45 | +**Hardcoded signing salt in endpoint.ex** | |
| 46 | +- `config/config.exs:229` — `live_view: [signing_salt: "FRxtZcCV"]` | |
| 47 | +- Salt is short (8 bytes) and appears to be a development default | |
| 48 | +- While salts don't need to be secret, short salts reduce brute-force resistance | |
| 49 | +- Recommendation: use environment-specific salts with at least 16 bytes | |
| 50 | +- Example: `System.get_env("LIVE_VIEW_SALT", "default-change-me")` | |
| 51 | + | |
| 52 | +**No session binding to client fingerprint** | |
| 53 | +- Sessions not bound to User-Agent or IP address | |
| 54 | +- Stolen session tokens can be used from different clients | |
| 55 | +- Recommendation: add optional IP binding or User-Agent validation for high-security deployments | |
| 56 | +- Note: this conflicts with some legitimate use cases (IP rotation, proxy rotation) | |
| 57 | + | |
| 58 | +**Cookie signing salt not configurable** | |
| 59 | +- `signing_salt` in `endpoint.ex` is hardcoded | |
| 60 | +- If compromised, all signed cookies can be forged | |
| 61 | +- Recommendation: make salt configurable via environment variable | |
| 62 | +- Example: `config :git_gud, GitGudWeb.Endpoint, signing_salt: System.fetch_env!("PHOENIX_SIGNING_SALT")` | |
| 63 | + | |
| 64 | +### Low | |
| 65 | + | |
| 66 | +**Session renewal window may be too long** | |
| 67 | +- 14-day expiry with 7-day renewal may be excessive for sensitive deployments | |
| 68 | +- Recommendation: make expiry configurable via instance settings | |
| 69 | +- Consider shorter lifetimes (e.g., 24 hours) for high-security instances | |
| 70 | + | |
| 71 | +**No concurrent session limit** | |
| 72 | +- Users can have unlimited concurrent sessions | |
| 73 | +- Recommendation: add optional concurrent session limit (e.g., 10 sessions per user) | |
| 74 | +- Include UI for managing active sessions with revocation capability |
Parents: 14fe8cf