Skip to content

Mount CONTAINER_WORKSPACE read-only by default - #103

Open
anticomputer wants to merge 1 commit into
mainfrom
workspace-readonly-default
Open

anticomputer wants to merge 1 commit into
mainfrom
workspace-readonly-default

Conversation

@anticomputer

Copy link
Copy Markdown
Contributor

Summary

container_shell bind-mounts CONTAINER_WORKSPACE at /workspace writable, so anything run through container_shell_exec can modify the host's copy of the tree under audit.

This adds CONTAINER_WORKSPACE_MODE, defaulting to ro, with rw as an explicit opt-in.

Why this is worth changing

The workspace is normally the source being analyzed, and consumers commonly collect it afterwards as an artifact. In our pipeline it lands in the audit bundle as workspaces/data/repo_under_test/ with 90-day retention, and human triagers read it when reviewing findings.

A writable mount makes that artifact agent-influenced rather than a faithful copy of the input. That is the wrong default for a tool whose entire purpose is examining untrusted code, and the shell is precisely the surface an injected prompt would reach. Narrowing it costs nothing today and removes a whole class of "is this artifact actually what upstream shipped?" doubt later.

To be clear about severity: this is not a sandbox escape, and I am not reporting it as a vulnerability. It is a default that grants more than anything needs.

Evidence that ro is safe as a default, not just as an option

I checked rather than assumed. Across a full audit run of the six taskflows that use this toolbox, all 144 container_shell_exec calls were reads:

tree /workspace -L 3 | head -80
cat /workspace/main.go
ls -la /workspace; git -C /workspace log --oneline | head -10
grep -n "HandleFunc\|ListenAndServe" /workspace/main.go
rg -n "exec\.|Command|host|sanitiz|valid|allow|regexp" /workspace --type go
cat -n /workspace/main.go
...

Not one write: no redirect into /workspace, no touch, no ctags, no build step. I also grepped the taskflow prompts for write/build verbs; the only hits (write to, make) are prose describing the audited application's behaviour, not instructions to the agent.

If a downstream taskflow does need writes, CONTAINER_WORKSPACE_MODE=rw restores the old behaviour exactly.

Design

Follows the existing CONTAINER_NETWORK precedent in this module, deliberately:

  • secure by default, explicit opt-in to widen
  • empty / unset / unrecognized falls back to the safe value, so the default cannot be silently weakened by a blank variable
  • only a literal rw (case-insensitive, whitespace-trimmed) opts in

Also folds the mode into _persistent_name() key material, for the same reason the network mode already is: a run configured for ro must not reuse a persistent container created with a writable workspace. That bug would otherwise be invisible and intermittent.

Note the existing colon guard on CONTAINER_WORKSPACE still applies, so the mode cannot be smuggled through the path.

Tests

62 passed. New coverage:

  • read-only default and rw opt-in at the docker run layer
  • env parsing: unset, blank, whitespace, bogus, readwrite, ro, rw, RW, rw, and an injection-shaped rw; rm -rf / (falls back to ro)
  • persistent-name separation between ro and rw

I verified the new assertions fail when the mount mode is removed (3 failed / 59 passed), so they detect the regression rather than merely passing alongside it.

One existing test changed: test_start_container_success asserts membership in the argument list, which is exact-element rather than substring, so it needed the :ro suffix.

Compatibility

Behaviour change for any consumer that writes to /workspace. I believe that is nobody today, but if you would rather not change the default, flipping it to opt-out is a one-line edit to the constant and I am happy to do that instead.

The workspace bind mount is writable, so a command run via
container_shell_exec can modify the host's copy of the tree under audit.

That tree is usually the source being analyzed, and callers commonly collect
it afterwards as an artifact. A writable mount therefore makes the collected
artifact agent-influenced rather than a faithful copy of the input, which is
the wrong default for a tool whose whole purpose is examining untrusted code.
The shell is also the one surface an injected prompt can reach, so the
capability is worth narrowing even though nothing exercises it today.

Adds CONTAINER_WORKSPACE_MODE, defaulting to "ro", with "rw" as an explicit
opt-in for any taskflow that genuinely needs to write.

This follows the CONTAINER_NETWORK precedent in this module exactly: secure by
default, opt-in to widen, and an empty or unrecognized value falls back to the
safe value so the default cannot be silently weakened by a blank variable.
Only a literal "rw" (case-insensitive, whitespace-trimmed) opts in.

The mode is also folded into the persistent-container key material, for the
same reason the network mode already is: a run configured for "ro" must not
reuse a container that was created with a writable workspace.

Evidence that read-only is a safe default rather than a guess: across a full
audit run of the six taskflows that use this toolbox, all 144
container_shell_exec calls were reads (tree, cat, ls, grep, rg, wc, git log,
sed -n). None wrote to /workspace, and no taskflow prompt instructs a write.

Tests: read-only default and rw opt-in at the docker run layer, env parsing
including blank/unrecognized/injection-shaped values, and persistent-name
separation between modes. Verified the new assertions fail when the mount mode
is removed, so they detect the regression rather than merely passing.

Note test_start_container_success changed: it asserts membership in the
argument list, which is exact-element rather than substring, so it needed the
new suffix.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

The writable opt-in is not propagated through bundled toolboxes, and persistent-name changes can orphan existing containers.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity · 1 Low severity

Open (3)
What changed in this PR

This PR makes /workspace mounts read-only by default while allowing explicit writable mode configuration.

Changes:

  • Adds workspace-mode parsing and Docker mount flags.
  • Includes mount mode in persistent-container naming.
  • Adds parsing, mount, and persistence tests.
File Description
src/​seclab_taskflows/​mcp_servers/​container_shell.py Implements read-only workspace mounts and mode-aware persistence.
tests/​test_container_shell.py Tests defaults, opt-in behavior, parsing, and name separation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +13 to +15
modify the host's copy of the source under audit. Set it to ``rw`` only if a
taskflow genuinely needs to write into the workspace. An empty, unset, or
unrecognized value falls back to ``ro``, so the default cannot be silently
Comment on lines +126 to +129
key_material = (
f"{CONTAINER_IMAGE}:{CONTAINER_WORKSPACE}"
f":net={CONTAINER_NETWORK}:ws={CONTAINER_WORKSPACE_MODE}"
)
finally:
_restore_env_and_reload("CONTAINER_WORKSPACE_MODE", original)

@pytest.mark.parametrize("value", ["", " ", "\t", "bogus", "readwrite", "ro"])
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.

3 participants