5.6 KiB · text History b868907

CI Security Review: GitGud

Critical

1. Insecure default secrets in config config/config.exs:141-149 — Two production-sensitive defaults shipped with the codebase:

  • Secrets master key: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" (all zeros). If runtime.exs doesn’t override this, all repository/organization secrets stored in CI are encrypted with a known key.
  • Runner JWT secret: "dev-runner-jwt-secret-do-not-use-in-prod" — forgeable runner tokens. An attacker with access to this could mint arbitrary runner tokens.

2. HS256-only JWT signing lib/git_gud/workflows/runner_jwt.ex:174 — Runner JWTs use symmetric HS256. The same secret signs all tokens. If leaked from any single runner, the attacker can mint tokens for any job. No key rotation mechanism exists at the JWT layer (only app-level master key rotation for secrets).

3. Fork PR secrets exposure lib/git_gud_web/controllers/runner_controller.ex:244-247 — Fork PRs correctly get zero secrets, which is good. However, ${{ github.token }} in the secrets map is injected at runner_controller.ex:213-215 as GITHUB_TOKEN, GITEA_TOKEN, FORGEJO_TOKEN. If a fork PR’s workflow references secrets.FORGEJO_TOKEN, it would get the job token rather than the user secret (correct), but the naming could confuse users about which token is which.

High

4. Unpinned marketplace action versions .forgejo/workflows/build.yml uses actions/checkout@v4, docker/setup-buildx-action@v3, docker/login-action@v3, etc. Major-version pinning (@v4) allows minor/patch updates that could introduce supply chain compromises. SHA pinning (@a7d94b...) is the defense-in-depth standard.

5. Self-hosted runner with full repo access Both build.yml:9 and test.yml:5 use runs-on: self-hosted with no container isolation. A malicious workflow could access the runner’s filesystem, other jobs’ artifacts, or secrets. Self-hosted runners should run in isolated containers with restricted capabilities.

6. Build cache exposed in registry .forgejo/workflows/build.yml:44-45 — Build cache is pushed to the OCI registry as <image>:buildcache with mode=max. This cache contains compiled source code and could be pulled by anyone with registry access. No access control is mentioned.

7. No SBOM or vulnerability scanning The entire build pipeline (build.yml) publishes Docker images and .deb/.rpm packages without any SBOM generation, image signing, or vulnerability scanning. Signed packages and signed container images are the supply chain baseline.

8. Android keystore shred reliability .forgejo/workflows/build.yml:253-254 — shred -u on always() runs even on failure. On tmpfs/overlayFS (common in container runners), shred is a no-op and the 2>/dev/null || rm -f fallback silently ignores shred failures. The keystore bytes may persist in the filesystem.

Medium

9. Expression injection surface lib/git_gud/workflows/expr.ex — The expression evaluator handles ${{ }} interpolation. While it doesn’t execute arbitrary code, the fromJSON function (expr.ex:307-311) decodes arbitrary JSON. If downstream code trusts fromJSON output without validation, malicious workflow payloads could craft edge-case inputs.

10. Runner registration token not expired lib/git_gud_web/controllers/runner_controller.ex:66-93 — Registration uses a one-shot token. However, there’s no evidence of token expiry enforcement in the controller. If a registration token is never consumed, it remains valid.

11. Large body read limits

  • actions_artifact_controller.ex:76 — read_body/2 with length: 1_000_000_000 (1 GB) per artifact upload. This can cause memory exhaustion if multiple concurrent uploads hit the limit.
  • actions_cache_controller.ex:91 — read_body/2 with length: 200_000_000 (200 MB) per chunk.
  • runner_controller.ex:700 — read_body/2 with length: 50_000_000 (50 MB).

12. Missing audit logging for secret operations Secrets can be created/updated/deleted via the web UI but there’s no explicit audit log for CI secret management operations.

13. update_task log-job fallback runner_controller.ex:571-588 — When task_id doesn’t match the runner’s claimed job, log_job/2 falls back to claimed_job/runner which attributes logs to whichever job the runner has running. If a runner has multiple concurrent jobs, log mixing could occur, obscuring what actually happened.

14. Pre-receive hook fail-open escape hatch The pre-receive hook (priv/scripts/gitgud-prereceive) has GITGUD_PREHOOK_FAIL_OPEN=1 as an escape hatch. If accidentally set in production, branch protection is bypassed.

15. Content-type spoofing in artifact upload actions_artifact_controller.ex:150 — Downloads return artifact.content_type from the database without validation. An attacker could set an arbitrary content_type during artifact creation, potentially enabling XSS if artifacts are served inline in a browser.

Low

16. Cache upload expiry is short actions_cache_controller.ex:46 — Upload expiry is 600s (10 min). If a runner times out before finalizing, the upload is orphaned. The CacheEviction worker (.gitlab-ci.yml:67 / config/config.exs:113-122) cleans abandoned uploads hourly, but in the meantime the storage is wasted.

17. No rate limiting on runner protocol The runner endpoints (/api/actions/_apis/runner.v1.*) have no rate limiting. A single runner could hammer FetchTask indefinitely, consuming DB resources.

18. Workflow file modification without branch protection .forgejo/workflows/ can be modified via direct push to master. No branch protection or required reviews are enforced on workflow file changes.