fix(auth): prevent repository .env files from injecting workspace bindings (Closes #704) #922
Open
jcwalker3
wants to merge 2 commits from
fix/issue-704-prevent-env-workspace-bindings into master
pull from: fix/issue-704-prevent-env-workspace-bindings
merge into: :master
:master
:fix/issue-987-native-mcp-bootstrap
:feat/issue-985-project-scoped-launcher-identity
:fix/issue-983-cross-repo-base-ref
:feat/issue-980-stale-worker-retirement
:fix/issue-975-client-identity-heartbeat
:fix/issue-973-cross-repo-canonical-roots
:fix/issue-970-safely-resolve-missing-worktrees
:fix/issue-969-native-mcp-bootstrap
:feat/issue-664-break-glass-restart
:feat/issue-708-mcp-namespace-attachment
:feat/issue-665-restart-audit
:fix/issue-700-durable-walls
:fix/issue-704-prevent-env-workspace-bindings
:feat/issue-707-cross-project-boundaries
:fix/issue-690-review-profile-switch-guard
:fix/issue-953-bootstrap-lock-provenance
:feat/issue-949-native-fleet-inventory
:fix/issue-943-runtime-context-helpers
:fix/issue-945-owning-pr-renewal-evidence
:fix/issue-941-scope-guard-bootstrap-wiring
:docs/issue-930-remote-mcp-coupling-inventory
:fix/issue-892-author-bootstrap-deadlock
:fix/issue-686-detect-reject-manual-mcp
:fix/issue-672-mcp-config-drift
:fix/issue-689-deterministic-mcp-namespace
:feat/issue-666-concurrent-mcp-restart-tests
:feat/issue-659-maintenance-drain-mode
:feat/issue-648-notifications-console
:fix/issue-670-direct-master-incident
:feat/issue-644-console-recovery
:feat/issue-650-providers-insights
:feat/issue-669-scoped-component-recovery
:docs/issue-668-mcp-ha-rolling-restart
:feat/issue-667-console-restart-controls
:feat/issue-645-linkage-console
:feat/issue-643-request-preview-initiate
:fix/issue-897-permission-stale-runtime-classification
:feat/issue-641-runtime-session-view
:feat/issue-663-restart-classes
:feat/issue-661-drain-proof-hard-gate
:fix/issue-854-semantic-container-exclusion
:issue-640
:fix/issue-682-starlette-httpx2
Labels
Clear labels
allocator
anti-stomp
architecture
bug
chore
codex
concurrency
contamination
control-plane
dashboard
database
design
documentation
enhancement
gitea
glitchtip
important
incident
incident-bridge
integration
jenkins
labels
leases
mcp
mcp-health
mcp-menu
multi-project
mutating
nice-to-have
observability
portability
preflight
protected-branch
queue
read-only
reconnect
recovery
refactor
release
reliability
resumable-review
reviewer
roadmap
safety
security
self-hosted
sentry
stale-runtime
status:blocked
status:in-progress
status:pr-open
status:ready
terminal-lock
testing
tracker
type:bug
type:feature
type:feature
type:guardrail
visibility
workflow
workflow-hardening
workflow-hardening
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
Controller-owned work allocator
Prevent concurrent LLM session stomping
Architecture / structural design
OpenAI Codex client / workflow session surface
Concurrent session safety
Workflow or session contamination incident
MCP control-plane coordination and allocation authority
MCP operational dashboard/queue view
Internal coordination storage (SQLite/Postgres)
Design / investigation, no implementation
Docs / runbooks
New feature or improvement
Gitea MCP workflow
GlitchTip integration
Operational or process incident requiring durable audit trail
Sentry-to-Gitea incident bridging
Integration testing
Jenkins integration
Label taxonomy management
Lease adopt/release/expire lifecycle
MCP server / tooling
MCP namespace and runtime health
MCP menu surface
Work spanning multiple monitoring projects or Gitea repos
Mutating action; requires gating
Observability, metrics, traces, error reporting
Cross-platform / portability
Shared preflight gates before mutation
Protected branch / stable-branch policy concern
Work queue visibility and allocation
Read-only, no mutation
MCP client reconnect/reload recovery path
Recovery paths for stale/foreign leases
Code refactor / restructure
Release / versioning
Reliability / failure handling
Persist and resume prepared review verdicts across sessions
Reviewer workflow tooling
Roadmap / umbrella issue
Safety rails and fail-closed mutation guards
Security / trust boundary
Self-hosted infrastructure integration
Sentry error monitoring integration
Stale backend daemon / runtime-vs-master parity failures
Issue is blocked
Issue is being worked on
Issue has an open pull request
Issue is ready for work
Terminal review lock (#332) path
Tests / test coverage
Issue tracker hygiene / meta
Bug or defect
Feature or enhancement
Feature or enhancement
Safety gate or guardrail
Workflow state visibility for LLMs/operators
Cross-tool workflow
LLM workflow coordination hardening
LLM workflow coordination hardening
Something is not working
This issue or pull request already exists
New feature
Need some help
Something is wrong
More information is needed
This won't be fixed
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Scaled-Tech-Consulting/Gitea-Tools#922
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Closes #704. Hardens repository
.envloading ingitea_auth.pyto prevent repository.envfiles from injecting or overridingGITEA_ACTIVE_WORKTREEor any role-specificGITEA_*_WORKTREEenvironment variables.Changes
1.
gitea_auth.pyRESERVED_WORKTREE_ENV_KEYSdefining all reserved runtime workspace binding environment variables (GITEA_ACTIVE_WORKTREE,GITEA_AUTHOR_WORKTREE,GITEA_REVIEWER_WORKTREE,GITEA_MERGER_WORKTREE,GITEA_RECONCILER_WORKTREEand anyGITEA_*_WORKTREEkey).is_reserved_worktree_env_key(key)helper.load_env_file_sanitized(env_path, target_env=None)to load.envfiles safely:.envfiles without populating or mutatingos.environ.DYNAMIC_CONFIGS) to strip reserved workspace binding keys.2. Unit Tests (
tests/test_issue_704_prevent_env_workspace_bindings.py)test_is_reserved_worktree_env_key: Verifies classification across all role namespaces (author, reviewer, merger, reconciler).test_load_env_file_sanitized_ignores_reserved_keys: Verifies.envloading ignores reserved keys while loading standard config (e.g.GITEA_USER).test_preexisting_sanctioned_launcher_env_retained: Verifies sanctioned process environment values retain precedence.test_stale_or_missing_worktree_in_env_ignored: Verifies stale/non-existent paths in.envare ignored.test_repeated_module_import_does_not_mutate_workspace_env: Verifies repeated imports ofgitea_authleaveos.environun-contaminated.test_namespace_isolation_all_roles_protected: Verifies protection across author, reviewer, merger, reconciler namespaces.test_absence_of_secret_leakage: Verifies rejection reasons never expose secret path values.Relationship to PR #703 & Issue #702
fb9191e55).Validation
python3 -m unittest tests/test_issue_704_prevent_env_workspace_bindings.py: 7 passed in 0.005s.PYTHONPATH=. pytest tests/test_issue_704_prevent_env_workspace_bindings.py: 7 passed in 0.70s.PYTHONPATH=. pytest tests/test_issue_702_stale_binding_lease_recovery.py: 29 passed.Worktree / Branch Proof
fix/issue-704-prevent-env-workspace-bindingsbranches/issue-704-prevent-env-workspace-bindingsef34d93Next Role
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #922
issue: #704
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18879-99a65ed7d43a
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr922-ef34d93-s2
phase: claimed
candidate_head:
ef34d938bftarget_branch: master
target_branch_sha:
c30b381eb2last_activity: 2026-07-27T16:04:05Z
expires_at: 2026-07-27T16:14:05Z
blocker: none
REQUEST_CHANGES — PR #922 at head
ef34d938bf419b4fc0f9a0c9efbd5c7f762a1a01Independent review by
sysadmin/prgs-reviewer; authorjcwalker3, so independence holds. Validated in a fresh session-owned worktreebranches/review-pr922-ef34d93-s2, detached at exactly this head, clean before and after. Merge base2b4e43042a34f4e29617378ae79a7f5a3d312688; targetmasterre-fetched atc30b381eb2756c4ba9cbb2bf7c3f2d6f31b701e7, head not an ancestor, so the already-landed gate does not fire.One blocker. The implementation is genuinely good on the criterion it targets, and one thing it does better than its sibling PRs deserves saying first.
What is correct
dotenventers this codebase at exactly two call sites —gitea_auth.py:62and:93— and this change covers both: the root.envload and theDYNAMIC_CONFIGSglob. Verified by grepping everyload_dotenv/dotenv_valuesoccurrence in production sources; there is no third path. That is the failure mode that sank several neighbouring PRs, and it is not present here.target_env:GITEA_ACTIVE_WORKTREEandGITEA_REVIEWER_WORKTREEwere both refused with sanitized reasons naming the key and the filename, never the value.GITEA_ACTIVE_WORKTREE=/sanctioned/wtin the target env, the.envvalue did not displace it — the sanctioned launcher value survived.is_reserved_worktree_env_keycorrectly handles lowercase (gitea_active_worktree), trailing whitespace, and theGITEA_*_WORKTREEsuffix pattern (GITEA_CUSTOM_ROLE_WORKTREEis caught without being enumerated), while correctly declining bareWORKTREE.B1 — BLOCKER: the reserved set covers workspace bindings only, so AC9 does not hold — a repository
.envcan still forge native-transport provenanceThe reserved set is five literals plus the
GITEA_*_WORKTREEsuffix rule. The actual domain is far larger: 107 distinctGITEA_*environment keys are referenced across production sources at this head, and 6 of them end in_WORKTREE. Everything else passes through.Reproduced by driving the real
load_env_file_sanitizedagainst a.envholding a mix of binding and runtime-control keys:Three of those admitted keys were then traced to the gates that read them, so this is not a list of scary-looking names:
GITEA_MCP_SANCTIONED_DAEMONmcp_daemon_guard.py:35SANCTIONED_DAEMON_ENVGITEA_ALLOW_DIRECT_MCP_IMPORTmcp_daemon_guard.py:36ALLOW_DIRECT_IMPORT_ENVGITEA_MCP_DISABLE_PARITY_GATEmaster_parity_gate.py:64ENV_DISABLE#704 AC9 is unambiguous: "No environment, dotenv, offline-import, or caller-controlled path can forge native transport or mutation provenance." A repository
.env— a file that arrives with agit checkoutand is exactly the vector #704 was filed about — still populates the two env varsmcp_daemon_guarduses to decide native-transport provenance. AC5 is also broader than the implementation: it says "Reserved runtime-control keys", andGITEA_MCP_DISABLE_PARITY_GATEis a runtime-control key by any reading.The PR body states
Closes #704, so landing this retires an open contamination-incident issue with AC5 and AC9 unmet. That is what makes this blocking rather than a scope note: the headline AC1 defect is fixed, but the issue's security boundary is not, and closing it would remove the tracking for the rest.On the fix direction. A denylist over a 107-key domain will keep losing to keys nobody enumerated — the same structural defect recorded as B1 on PR #908. The safer inversion is an allowlist: repository
.envfiles supply configuration (GITEA_HOST,GITEA_USER, site/credential wiring forDYNAMIC_CONFIGS), so admit that set and refuse unrecognizedGITEA_*keys with the sanitized reason already implemented. If an allowlist is judged too disruptive, then at minimum extend the reserved set to every provenance, gate-disabling, authorization-override, and state-redirection key, and add a test that fails when a new one is introduced.Why the green suite did not catch it:
tests/test_issue_704_prevent_env_workspace_bindings.py(7 passed at this head) exercises only_WORKTREEkeys plus benignGITEA_USER/GITEA_PASS/GITEA_TOKEN/GITEA_HOST. No test asserts that any runtime-control key is refused, so the reserved set can never fail a test by being too narrow.One claim I checked and am not making:
GITEA_BYPASS_NATIVE_MCPis admitted, but at this head it appears only intests/test_structured_auth_mcp_errors.py:377and in no production source, so I am not citing it as a live bypass. The three in the table above are the load-bearing ones.Non-blocking observations
load_dotenvis still imported atgitea_auth.py:19but no longer called anywhere — the sanitized loader replaced its only use. Removing it would prevent a future edit from reaching for the unsanitized function that sits conveniently in scope.load_env_file_sanitizedwraps its whole body inexcept Exception: passand returns[]. A malformed.envis then indistinguishable from an absent one, and the rejection reasons that AC5 requires are silently lost. Consider narrowing the catch or surfacing a parse-failure reason.rejection_reasonslist has no caller — the module-level invocation atgitea_auth.py:80discards it, so AC5's "rejected with a sanitized actionable reason" is satisfiable in principle but not actually surfaced anywhere an operator would see. Worth logging or exposing.Validation
Official validation status:
baseline-equivalent failure accepted. The seven failures reproduce at the merge base with a byte-identical set of failing ids, so none originates here. No validation failure beyond that baseline was observed in this session. No full-suite run was performed at this head and none is claimed. No file in either worktree was edited; the B1 reproduction used a scratch.envoutside the repository tree and a throwawaytarget_envdict.Canonical PR State
STATE: PR #922 is open at head
ef34d938bfand now carries a formal REQUEST_CHANGES verdict from sysadmin recorded at that exact head. One blocking finding (B1) is open alongside three non-blocking observations. The change introduces no test regression against merge base2b4e43042a.WHO_IS_NEXT: author
NEXT_ACTION: Author jcwalker3 must extend the sanitizer to cover runtime-control and provenance keys so #704 AC5 and AC9 hold, preferably by inverting to an allowlist, add a regression test per admitted key class, push, and publish a new head-pinned handoff for a fresh independent review.
NEXT_PROMPT:
WHAT_HAPPENED: An independent review at the exact head read both changed files, enumerated every dotenv entry point in production sources and confirmed the change covers all of them, then drove the real sanitizer against a scratch .env to establish what it refuses and what it admits. Two workspace-binding keys were refused as designed and nine other keys were admitted, three of which were traced to the daemon-provenance guard and the master-parity gate that consume them. Sanctioned-value precedence and key-shape normalisation were confirmed correct by the same method. The focused suite passes at head and the neighbouring sweep shows an identical failing id set at head and at the merge base.
WHY: #704 exists because a repository .env poisoned MCP runtime bindings, and its acceptance criteria extend past workspace paths to forbid any dotenv path forging native transport or mutation provenance. The change fully fixes the workspace-binding half and correctly wires both dotenv call sites, but a denylist scoped to six suffix-matched keys leaves the provenance and gate-disabling keys reachable from the same file, so the criterion the issue was filed to guarantee does not yet hold while the PR would close the issue.
ISSUE: #704
HEAD_SHA:
ef34d938bfREVIEW_STATUS: REQUEST_CHANGES recorded at
ef34d938bfby sysadminMERGE_READY: no
BLOCKERS: code blocker
VALIDATION: Reviewed in /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr922-ef34d93-s2, created fresh this session, detached at
ef34d938bf, verified clean by git status --porcelain --untracked-files=all before and after. Target branch master re-fetched from prgs at c30b381eb2756c4ba9cbb2bf7c3f2d6f31b701e7; git merge-base --is-ancestor reports the head is not an ancestor. Diff against merge base2b4e43042ais 2 files, +228/-4. Every dotenv entry point in production sources was enumerated by grep and found to be exactly gitea_auth.py:62 and gitea_auth.py:93, both covered by this change. The GITEA_ key domain was enumerated across production and test sources at 107 distinct keys, of which 6 end in _WORKTREE. B1 was reproduced by invoking the real load_env_file_sanitized with a scratch .env outside the repository tree and a throwaway target_env dict: 2 workspace keys refused with sanitized reasons, 9 keys admitted including GITEA_MCP_SANCTIONED_DAEMON, GITEA_ALLOW_DIRECT_MCP_IMPORT, GITEA_MCP_DISABLE_PARITY_GATE, GITEA_ALLOW_STALE_RUNTIME, GITEA_BREAKGLASS_RESTART_AUTHORIZATION, GITEA_MCP_PROFILE, GITEA_MCP_SESSION_STATE_DIR and GITEA_ACCESS_TOKEN. Those admitted keys were traced to mcp_daemon_guard.py:35, mcp_daemon_guard.py:36 and master_parity_gate.py:64. GITEA_BYPASS_NATIVE_MCP was found only in tests/test_structured_auth_mcp_errors.py:377 with no production consumer and is therefore explicitly not claimed as a live bypass. AC3 precedence was confirmed by pre-seeding target_env with a sanctioned GITEA_ACTIVE_WORKTREE value and observing it survive. Key normalisation was checked across lowercase, trailing-whitespace, suffix-pattern and bare-WORKTREE inputs. Focused suite at head: 7 passed. Neighbouring sweep at head: 7 failed, 798 passed, 4717 deselected, 271 subtests. Same sweep at the merge base in branches/baseline-pr922-2b4e430-s2, run in a command block carrying its own cd, pwd and git rev-parse HEAD: 7 failed, compared with comm in both directions and found identical, so no regression originates from this branch. No full-suite run was performed at this head and none is claimed. No file was edited in either worktree. Pushes during validation: none.LAST_UPDATED_BY: sysadmin / prgs-reviewer / gitea-reviewer namespace, reviewer lease session 18879-99a65ed7d43a
repo: Scaled-Tech-Consulting/Gitea-Tools
pr: #922
issue: #704
reviewer_identity: sysadmin
profile: prgs-reviewer
session_id: 18879-99a65ed7d43a
worktree: /Users/jasonwalker/Development/Gitea-Tools/branches/review-pr922-ef34d93-s2
phase: released
candidate_head:
ef34d938bftarget_branch: master
target_branch_sha:
c30b381eb2last_activity: 2026-07-27T16:07:38Z
expires_at: 2026-07-27T16:17:38Z
blocker: manual-release
View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.