Skip to content

fix: security fixes for 1.1.4 - #49

Merged
freema merged 5 commits into
mainfrom
fix/security-1.1.4
Oct 2, 2026
Merged

freema merged 5 commits into
mainfrom
fix/security-1.1.4

Conversation

@freema

@freema freema commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Security fixes for 1.1.4. Each commit is self-contained and comes with a regression test that fails without it.

Changes

Tenant ownership on session routes (fix(server))
The ownership middleware was mounted where chi had not resolved {sessionID} yet, so the check never ran. The {sessionID} routes now live in their own subrouter with the middleware attached there. Test: TestSessionRoutes_TenantCannotReachForeignSession (router-level, all nine routes).

GitLab MR note commands need Developer access (fix(webhooks))
Note hooks don't carry the author's role, so /review, /fix and /fix-cr were dispatched for anyone who could comment on a same-project MR. The author's effective access level is now looked up through the members API (members/all) and must be ≥ 30, mirroring the GitHub author_association check. Any lookup failure refuses the command. Tests: TestGitLabNoteCommandRequiresDeveloperAccess, TestGitLabAccessLevel (local fake API).

Server-side git treats the workspace as untrusted (fix(git))
Git run by the server in a session workspace could be steered by configuration the AI CLI wrote there, and it inherited the server environment. All server-side git now goes through git.Command, which:

  • pins program-running and submodule settings with -c;
  • ignores the global config;
  • allowlists the environment.

SanitizeRepoConfig rebuilds .git/config from an allowlist before use and refuses redirected .git directories. Fetch and push use the URL the session was cloned from, and the askpass helper answers only for that host. Tests:

  • TestServerGitIgnoresWorkspaceExecutors
  • TestPushIgnoresWorkspaceOrigin
  • TestCommandEnvironmentExcludesServerSecrets
  • TestSanitizeRepoConfig
  • TestAskPassScriptOnlyAnswersRepoHost

Tenant sessions don't use operator credentials (fix(subscription))
In subscription mode, a tenant session could end up with operator-owned access through:

  • registered keys;
  • the env token fallback;
  • tool auto-fill;
  • operator MCP servers;
  • local repository paths;
  • another tenant's workspace.

All of these are now closed for sessions with a tenant. Operator sessions are unchanged. Tests:

  • TestApplyTenant_RejectsForeignCredentials
  • TestResolveToken_TenantSessionGetsNoOperatorToken
  • TestSetupMCP_TenantSessionGetsNoOperatorCredentials
  • TestResolveAccessToken
  • TestResolver_ResolveOwnConfigSkipsAutoFill

Codex prompt can't inject options (fix(runner))
-- is now passed before the prompt. Test: TestCodexArgs_PromptCannotInjectOptions.

Behaviour changes for operators

  • GitLab: the code_review.default_key_name token must be able to read project members (read_api/api). If it can't, note commands are refused.
  • Git config: server-side git no longer reads ~/.gitconfig. Set TLS trust for self-hosted instances with GIT_SSL_CAINFO / SSL_CERT_FILE or /etc/gitconfig. Dev and action images set safe.directory there; it is now passed on the command line instead.
  • Subscription tenants:
    • provider_key → 403; repo_url must be http(s).
    • Private repositories, PR creation and comment posting need the tenant's own access_token.
    • The operator's registered MCP servers are not added to tenant sessions.

Release notes draft (v1.1.4)

Security release. Upgrade from v1.1.3 or earlier is recommended for every deployment; subscription mode and GitLab webhook setups are the most affected.

Session isolation:

- Subscription tenants can reach only their own sessions on every `/sessions/{id}` route.
- Tenant sessions no longer fall back to operator credentials (registered keys, GITHUB_TOKEN/GITLAB_TOKEN, tool auto-fill, operator MCP servers); `provider_key` is rejected, `repo_url` must be http(s), and `workspace_session_id` must name the tenant's own session.

Webhooks:

- GitLab MR note commands (`/review`, `/fix`, `/fix-cr`) require Developer access or higher, looked up through the GitLab API with the `default_key_name` token.

Git:

- Git run by the server in a session workspace ignores workspace-controlled configuration and the global git config, runs with an allowlisted environment, and fetches/pushes only to the URL the session was cloned from. The askpass helper answers only for that host.

Runner:

- Codex prompts are passed after `--`, so they cannot be read as CLI options.

Upgrade notes:
- The GitLab `default_key_name` token needs `read_api` (or `api`).
- Server-side git ignores `~/.gitconfig`; use `GIT_SSL_CAINFO` / `SSL_CERT_FILE` or `/etc/gitconfig` for custom CAs.
- Tenant sessions need their own `access_token` for private repositories and PRs.

Images: `ghcr.io/freema/codeforge:v1.1.4`, `ghcr.io/freema/codeforge-action:v1.1.4`, `ghcr.io/freema/codeforge-ui:v1.1.4` (also tagged `latest`).

🤖 Generated with Claude Code

freema and others added 5 commits October 2, 2026 11:40
OwnershipMiddleware was attached with r.Use on the /sessions subrouter,
where chi has not matched {sessionID} yet. chi.URLParam returned an empty
string, so the check passed every request and a subscription tenant could
read and act on any other tenant's session by ID.

Mount the {sessionID} routes in their own subrouter and attach the
middleware there, so the param is resolved when the check runs. The
lookup is injectable (SessionOwnership) so the wiring is covered by a
router-level test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitLab note hooks do not carry the author's role, so /review, /fix and
/fix-cr on a same-project MR were dispatched for anyone able to comment,
including users with no access to the code. /fix forwards the note body
as the prompt of a code-writing session.

Look up the author's effective access level through the GitLab members
API (members/all, so group membership counts) and dispatch only for
Developer (30) or above, mirroring the GitHub author_association check.
Missing IDs, a missing lookup or an API error refuse the command. The
lookup refuses redirects so the token stays on the instance host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server ran git in workspaces the AI CLI had written to, with the
server's full environment. Workspace-controlled configuration (fsmonitor,
hooks, filter and diff drivers, include files, the shared ~/.gitconfig)
could run programs that received CODEFORGE_* secrets and provider tokens,
and a changed origin URL or insteadOf rewrite made the askpass helper hand
the push token to another host.

All server-side git now goes through git.Command, which:
- pins settings that run programs or recurse into submodules with -c
  (fsmonitor, hooksPath, credential.helper, askPass, submodule recursion,
  ext/file protocol policy, gpg signing) and sets safe.directory there;
- ignores the global config (GIT_CONFIG_GLOBAL=/dev/null);
- builds the environment from an allowlist (PATH, HOME, TLS, proxy).

SanitizeRepoConfig rebuilds .git/config from an allowlist before git runs
in a workspace and refuses a .git that is a symlink, a gitfile or has a
commondir. Fetch and push reset origin to the URL the session was cloned
from. The askpass script answers only prompts for that scheme and host.
Commits and pushes also pass --no-verify, diffs --no-ext-diff
--no-textconv.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A tenant session could reach the operator's access in several ways:
naming a registered key in provider_key, falling back to the
GITHUB_TOKEN/GITLAB_TOKEN of the server when it brought no token, having
tool config auto-filled from registered keys, getting the operator's
registered MCP servers (with their env and headers) written into its
workspace, cloning a local path through a file:// repo_url, or reusing
another tenant's workspace through workspace_session_id.

applyTenant now rejects provider_key, non-HTTP(S) repo URLs and
workspace_session_id values that do not name one of the tenant's own
sessions. Sessions with a TenantID (Session.UsesOperatorCredentials)
skip the token fallback in the executor and PR service, resolve tools
without auto-fill (tools.Resolver.ResolveOwnConfig) and get only their
own MCP servers. Operator sessions are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The prompt was the last positional argument of `codex exec` with no "--"
in front of it, so a prompt starting with "-" was parsed as an option:
"-cmodel_provider=..." became a config override. Pass "--" before the
prompt and cover the argument list with a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@freema
freema merged commit c87e2cf into main Oct 2, 2026
10 checks passed
@freema
freema deleted the fix/security-1.1.4 branch October 2, 2026 10:17
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.

1 participant