Skip to content

security: full DevSecOps hardening — fix 10 vulnerabilities across application, container, and CI/CD layers - #4

Merged
t0mer merged 11 commits into
mainfrom
security
Jun 2, 2026
Merged

t0mer merged 11 commits into
mainfrom
security

Conversation

@t0mer

@t0mer t0mer commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Summary

Full security audit and remediation of the DeCompose application.
10 vulnerabilities fixed across 4 layers (application, dependencies,
container, CI/CD), each in an isolated commit.

Fixes

Commit Severity Finding Fix
2514e2d HIGH sys.exit(1) in request handler terminates the uvicorn process on any unknown container name — unauthenticated one-request DoS Raise HTTPException(404) instead; tighten container-matching
from substring to exact equality
69b1dbf HIGH Path traversal in /api/download — cname used directly as a filename; old substring match x.short_id in cname allowed abc123/../../etc/backdoor to bypass validation Validate cname
against Docker's allowed charset [a-zA-Z0-9_.-]; write output to NamedTemporaryFile
e4da7dd HIGH CORS allow_origins=["*"] + allow_credentials=True causes Starlette to reflect the requesting Origin verbatim, allowing any cross-origin page to make credentialed requests and read full API
responses including container secrets Remove CORS middleware (app serves its own frontend from the same origin); disable public Swagger UI/ReDoc (docs_url=None, redoc_url=None)
20884a1 MEDIUM TOCTOU race on shared docker-compose.yaml — concurrent requests could read each other's output, leaking container secrets across callers; secret-containing files accumulated on disk
indefinitely Return pyaml.dump(data) string directly — no file I/O needed
0b603dd MEDIUM No security response headers — missing X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Content-Security-Policy Add SecurityHeadersMiddleware setting all four headers on
every response
c41144b HIGH docker==4.4.4 (4 years old) and pyaml==20.4.0 (locks transitive PyYAML to pre-CVE-2020-14343 range); fastapi/uvicorn/loguru inherited unversioned from base image Update to
docker==7.1.0, pyaml==24.4.0; pin all runtime deps explicitly
aa22196 HIGH Dockerfile used techblog/fastapi:latest — unverified custom image on a mutable tag (supply-chain risk); container ran as root (uid 0) Switch to python:3.12-slim; add appuser (uid 10001)
with USER directive; add HEALTHCHECK
1a9adc9 MEDIUM No .dockerignore — full repo sent to Docker daemon as build context; .env files or credentials present at repo root would be silently embedded in published images Add .dockerignore
excluding .git, .env*, CLAUDE.md, .claude/, docs, screenshots
07d108e HIGH Docker socket mounted read-write (app only needs read access); no capability drops; no no-new-privileges; no healthcheck — combined with running as root, any RCE in the app yields full host
compromise via the socket Add :ro to socket mount; cap_drop: ALL; security_opt: [no-new-privileges:true]; read_only: true with /tmp tmpfs; healthcheck
8158de0 MEDIUM GitHub Actions pinned to mutable tags (@v3, @v4) — supply-chain compromise could silently execute malicious code in CI; JCR login interpolated token directly into a shell command, exposing
it in Actions debug logs Pin all 5 actions to immutable commit SHAs; replace inline docker login shell with docker/login-action

Test plan

  • GET /api/generate?cname=doesnotexist returns HTTP 404, server stays up
  • GET /api/download?cname=abc123/../../../etc/passwd returns HTTP 400
  • Cross-origin credentialed fetch to /api/containers is rejected by the browser
  • /docs and /redoc return HTTP 404
  • Response headers include X-Frame-Options: DENY and Content-Security-Policy
  • Docker image builds successfully from python:3.12-slim
  • docker exec decompose id shows a non-root user (uid 10001)
  • Docker socket is mounted :ro and the container starts and passes healthcheck

t0mer added 11 commits June 2, 2026 04:16
- Replace deprecated ::set-output with $GITHUB_OUTPUT
- Bump actions/checkout v2 → v4
- Bump docker/setup-qemu-action v1 → v3
- Bump docker/setup-buildx-action v1 → v3
- Bump docker/login-action v1 → v3
- Bump docker/build-push-action v2 → v6
- Use --password-stdin for JCR docker login
sys.exit(1) inside a FastAPI handler raises SystemExit which propagates
past ASGI exception handling and terminates the uvicorn process. Any
unauthenticated caller with an unknown container name could crash the
server. Raise HTTP 404 instead.

Also tightens the container-matching predicate from 'x.short_id in cname'
(substring match - exploitable for path traversal) to exact equality,
as a prerequisite for the path-traversal fix.
The cname parameter was used directly in open(cname + '-docker-compose.yaml')
without sanitization. Because the old container-matching logic used a substring
check ('x.short_id in cname'), an attacker could pass a value like
'abc123/../../etc/cron.d/backdoor' to write files at arbitrary paths.

Fix: validate cname against Docker's allowed character set [a-zA-Z0-9_.-]
before processing, and write output to a secure tempfile rather than a
user-controlled path.
…gger UI

Starlette's CORSMiddleware reflects the requesting Origin verbatim when
allow_credentials=True is combined with allow_origins=["*"], allowing any
cross-origin page to make credentialed requests and read API responses
(including container secrets). The app serves its own frontend from the
same origin, so no CORS middleware is needed at all.

Also disables the FastAPI Swagger UI and ReDoc endpoints (docs_url=None,
redoc_url=None) which provided an unauthenticated attack map of all
API endpoints, and removes the duplicate FastAPI instantiation.
The endpoint wrote all container data (including env vars with secrets)
to a single shared file 'docker-compose.yaml' then read it back.
Concurrent requests could read each other's output, disclosing secrets
between callers. Files were also never cleaned up, accumulating secrets
on disk indefinitely.

pyaml.dump() returns the YAML string directly when no stream is given,
so no intermediate file is needed.
Adds X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and
Content-Security-Policy headers to all responses, preventing MIME
sniffing, clickjacking, and restricting resource loading to the
same origin.
- docker 4.4.4 → 7.1.0: 4 year old SDK with security fixes across 5.x-7.x
- pyaml 20.4.0 → 24.4.0: resolves transitive PyYAML pre-CVE-2020-14343 range
- Add explicit pins for fastapi, uvicorn, loguru previously inherited from
  the techblog/fastapi base image without version control
- Remove six (no longer required by docker-py 7.x)
- Switch FROM techblog/fastapi:latest (unverified custom image, mutable tag)
  to python:3.12-slim (official Docker image, verified publisher)
- Add non-root user (appuser, uid 10001) to limit blast radius if the
  application is compromised
- Add HEALTHCHECK so orchestrators can detect crashed app state
- Set PYTHONDONTWRITEBYTECODE/PYTHONUNBUFFERED/PIP_NO_CACHE_DIR for
  leaner, more predictable image builds
Without a .dockerignore the full repo (including .env files, .git history,
AI assistant context, and documentation) is sent to the Docker daemon as
build context. A mistaken 'COPY . .' would silently embed credentials or
internal tooling configuration into published images.
- Mount Docker socket read-only (:ro) — app only needs read access;
  write access allowed container-to-host escape via socket API
- Drop all Linux capabilities (cap_drop: ALL) — no capabilities needed
- Add no-new-privileges to block setuid escalation
- Set read_only filesystem with /tmp tmpfs for writes the app needs
- Add healthcheck so orchestrators detect crashed application state
- Change restart policy to unless-stopped (avoid restart loops on crash)
Mutable version tags (e.g. @V3) can be silently updated by a supply-chain
attacker to run malicious code in CI. Pin all actions to their immutable
commit SHAs:
  actions/checkout          11bd71901bbe5b1630ceea73d27597364c9af683 (v4.2.2)
  docker/setup-qemu-action  49b3bc8e6bdd4a60e6116a5414239cba5943d3cf (v3.2.0)
  docker/setup-buildx-action c47758b77c9736f4b2ef4073d4d51994fabfe349 (v3.7.1)
  docker/login-action       9780b0c442fbb1117ed29e0efdff1e18412f7567 (v3.3.0)
  docker/build-push-action  4f58ea79222b3b9dc2c8bbdd6debcef730109a75 (v6.9.0)

Also replaces the JCR inline shell login (which interpolated the token
directly into a shell command, exposing it in debug logs) with
docker/login-action, which handles credentials out of the shell context.
Copilot AI review requested due to automatic review settings June 2, 2026 04:39
@t0mer
t0mer merged commit 1e0857b into main Jun 2, 2026
1 check was pending
@t0mer

t0mer commented Jun 2, 2026

Copy link
Copy Markdown
Owner Author

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@t0mer
t0mer deleted the security branch June 2, 2026 04:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR applies a multi-layer security hardening pass across the DeCompose FastAPI app, its containerization, and CI workflows, addressing reported vulnerabilities (DoS, path traversal, CORS misconfig, supply-chain pinning, and container runtime restrictions).

Changes:

  • Hardens FastAPI endpoints and defaults: safer container lookup + 404s, removal of permissive CORS, disables Swagger UI/ReDoc, adds security response headers, avoids shared compose-file I/O.
  • Strengthens container security posture: switches to python:3.12-slim, runs as non-root, adds healthchecks, and tightens docker-compose runtime settings (read-only FS, drop caps, no-new-privileges, read-only docker.sock).
  • Hardens CI supply chain: pins GitHub Actions to immutable SHAs and avoids token interpolation in shell for registry login.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Dockerfile Switch to official Python base, install pinned deps, run as non-root, add healthcheck
docker-compose.yaml Harden runtime settings (RO socket, cap drop, RO FS, tmpfs, healthcheck)
decompose/requirements.txt Pin runtime deps (but currently missing jinja2; see comments)
decompose/decompose.py Fix DoS behavior, remove risky CORS, add security headers, safer download handling
.github/workflows/main.yml Pin actions by SHA; modernize outputs; use login-action
.github/workflows/docker-image.yml Pin actions by SHA; use login-action + build-push-action
.dockerignore Reduce build context; exclude sensitive/unneeded files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread decompose/decompose.py
Comment on lines +177 to +180
with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as tmp:
pyaml.dump(data, tmp)
tmp_path = tmp.name
return FileResponse(tmp_path, media_type='application/octet-stream', filename=filename)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants