Skip to content

fix(security): R-11 — the image's code is not writable by the process that runs it - #529

Merged
jwvanderstam merged 7 commits into
mainfrom
fix/r11-image-source-root-owned
Nov 11, 2026
Merged

jwvanderstam merged 7 commits into
mainfrom
fix/r11-image-source-root-owned

Conversation

@jwvanderstam

Copy link
Copy Markdown
Owner

What

QP-9 review finding R-11 (Low): the image's source was owned by the runtime user. The virtual environment was too. So code execution in the app could persist by rewriting a module, or by dropping a new one into /app (first on sys.path) that shadows any import.

Change

  • Dockerfile:
    • The source and /opt/venv are copied without --chown, so root owns them.
    • USER 0:0 at the start of the runtime stage. The hardened base already runs as 65532, and WORKDIR creates its directory as the current user, so without this /app itself stayed writable. I found that by building the image and checking; it is the second commit.
    • The runtime user writes only /app/logs, /app/uploads and a new /app/data.
    • The image sets STATE_FILE=/app/data/app_state.json and RERANKER_MODEL_PATH=/app/data/reranker/latest.
  • src/config.py: STATE_FILE is read from the environment. The default stays app_state.json, so host runs are unchanged.
  • src/rag/feedback_pipeline.py: a fine-tune writes its versions beside RERANKER_MODEL_PATH, where promote_model's latest.txt pointer and the reranker look. Before, it wrote to a fixed ./models/reranker, which is no longer writable in the image.
  • docker-compose.yml: a new app_data volume on /app/data. A fresh volume is seeded from the image, owned by 65532, so no upgrade chown is needed.
  • docker-smoke: a new step asserts write access in the built image: /app, /app/src, a module and /opt/venv/lib read-only; logs, uploads and data writable.

Behaviour change (CHANGELOG, Changed)

The active model and RAG parameters now survive a container recreation. They were lost on every up -d --build (ROADMAP's accepted-debt entry). When upgrading, values held in the old container are not carried over and must be set once more. A fine-tuned reranker in the old container falls back to the base model until retrained. The ROADMAP entry's real fix, an app_settings table, still stands for coherence; its Docker bullet is annotated.

Verification

  • Built locally as localchat:r11-smoke (the live stack untouched):
    • uid 65532;
    • the write-access matrix exactly as asserted, with nothing wrong;
    • src.config imports with production variables, and set_active_model writes /app/data/app_state.json.
  • Unit: a fine-tune without output_dir writes beside RERANKER_MODEL_PATH and points latest.txt at it. The config-doc, env-example and compose tests pass, and ruff is clean.
  • The docker-smoke boot and security-smoke (full compose) will exercise the rest in CI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx

Copilot AI balanced review requested due to automatic review settings November 8, 2026 20:43

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

jwvanderstam and others added 2 commits November 8, 2026 22:35
… that runs it

The source and the virtual environment were owned by uid 65532, the user the
app runs as, so code execution in the app could persist by rewriting a module.
Both now belong to root. The runtime user writes only /app/logs, /app/uploads
and a new /app/data, which holds the settings page's state (STATE_FILE, new,
set by the image) and fine-tuned rerankers (RERANKER_MODEL_PATH, set by the
image). A fine-tune now writes beside RERANKER_MODEL_PATH rather than a fixed
./models/reranker. Compose mounts app_data, so the state also survives a
container recreation, which it never did.

docker-smoke asserts each path's write access in the built image.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
The hardened base already runs as 65532, and WORKDIR creates its directory as
the current user, so /app was still the runtime user's even with its contents
owned by root: a module dropped there would shadow any import, since /app is
first on sys.path. Found by building the image and checking write access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
@jwvanderstam
jwvanderstam force-pushed the fix/r11-image-source-root-owned branch from 1767602 to db306a1 Compare November 8, 2026 21:35
Copilot AI balanced review requested due to automatic review settings November 8, 2026 21:35

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

From the separate review of #529. A model named by the pointer file or a
version row is loaded only if it lies beside RERANKER_MODEL_PATH, and no
model may bring its own code (trust_remote_code=False, explicitly). A
promotion checks that first and writes the pointer before activating the
row, so a version trained into ./models before this change is refused
rather than marked active while the old model keeps serving. docker-smoke
now walks /app and /opt/venv and fails on anything writable outside logs,
uploads and data, on anything writable on sys.path, and on user site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkWPqAcWbxfqz1aCP6Qsdx
@jwvanderstam

Copy link
Copy Markdown
Owner Author

Separate security review (CONTRIBUTING / QP-9)

A separate agent session reviewed this PR. It built the image under a throwaway tag and walked the filesystem as uid 65532. No finding at Medium or above.

  • Writable for 65532: /app/data, /app/logs, /app/uploads, plus the base's HOME, /tmp, /var/tmp and /var/lock.
  • None of those is on sys.path. PYTHONPATH is unset, and ENABLE_USER_SITE is False, so a planted .pth/usercustomize under HOME never loads.
  • A fresh app_data volume is seeded with uid 65532.
  • Every runtime write (uploads, logs, /tmp, the state file's tmp-then-rename, fine-tune output) still lands somewhere writable.

Findings and answers:

Finding Answer
Low: the reranker loaded whatever latest.txt or a version row named, so code execution could persist a tampered model on the app_data volume. Weights only under the pinned transformers/torch defaults. Fixed in the follow-up commit. confined_model_path loads a pointed or promoted model only if it lies beside RERANKER_MODEL_PATH, and every CrossEncoder gets trust_remote_code=False explicitly. tests/unit/test_reranker_model_is_confined.py.
Low: promoting or rolling back to a version trained into ./models before the upgrade activated the row, then failed to write the pointer, leaving the database and the running model out of step. Fixed: the version's model must exist inside the model directory, and the pointer is written before the row is activated. The CHANGELOG upgrade note says so.
Low: the smoke assertion sampled seven paths. Fixed: it walks /app and /opt/venv, and fails on anything writable outside logs/uploads/data, on anything writable on sys.path, and on user site. Verified locally against a fresh build (no problems).
Info: STATE_FILE/RERANKER_MODEL_PATH are operator-set; pointing them off the volume degrades, it does not crash. Accepted.
Info: DEPLOYMENT.md still said "both paths". Fixed: logs and uploads are the volumes that may predate the hardened image; app_data is always fresh.
Info: settings in the old container are lost on upgrade, no worse than any recreation before. Agreed; a minor release fits.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings November 8, 2026 21:42

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Conflicts were side-by-side additions to CHANGELOG, file-map and the docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 11, 2026 00:08

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 11, 2026 01:18

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 11, 2026 02:03

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings November 11, 2026 02:47

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@sonarqubecloud

Copy link
Copy Markdown

@jwvanderstam
jwvanderstam merged commit 04fdd45 into main Nov 11, 2026
17 checks passed
@jwvanderstam
jwvanderstam deleted the fix/r11-image-source-root-owned branch November 11, 2026 03:30
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