Repository navigation
fix(studio): P0b refuse .git landing paths + harden server git invocations - #522
Open
brianonbased-dev wants to merge 1 commit into
Open
brianonbased-dev wants to merge 1 commit into
brianonbased-dev wants to merge 1 commit into
Conversation
…tions Pre-existing on main (not introduced by #521): workspaceFs blocked `.git` by request text only. A clone carrying `gitalias -> .git` let POST /api/workspace/files write `gitalias/config` (200) over `.git/config`; setting core.fsmonitor there makes the next server-side `git status` run that program as the Studio user. Reproduced with real git 2.47.3. Guard 1 — refuse .git by where the path LANDS, not its text (lib/workspace/workspaceFs.ts): hasGitMetadataSegment() (any depth, case-insensitive, trailing dot/space tolerant; `.gitignore` / `my.git.txt` are fine). resolveInsideWorkspace now refuses when the lexical target, the realpathed deepest existing ancestor, or the followed landing path has a `.git` segment, so every write/move/delete/mkdir caller (files, paper-opt-in, daemon patch targets) is covered; reads too (files read uses the same resolver). git/tree refuses to list a `.git` (direct or via symlink). assertWorkspaceOwner refuses a workspace path that is or sits inside `.git`. Guard 2 — one hardened git helper (lib/git/safeGit.ts). Every server git call runs with `-c core.fsmonitor=false -c core.hooksPath=/dev/null -c protocol.ext.allow=never` (command-line config overrides repo/global/ system and is inherited by git subprocesses). Routed: all /api/git/* routes (status, branch, commit, diff, blame, push, ship, tree), workspace/import clone, daemon/jobs store apply-to-branch, existingWorkspaceImporter, provisionUser, founderWorkspaceBackfill. (GIT_CONFIG_NOSYSTEM and core.sshCommand deliberately omitted; see PR body.) Tests (real git in temp dirs): lib/workspace/__tests__/gitHardening.p0b.test.ts and lib/git/__tests__/safeGit.test.ts. Five behavioral tests are red on 5a8f4cc and green here; negative controls remove each guard and the right tests (incl. the full end-to-end chain) go red.
Owner
Author
HoloCI verdict: NOT JUDGEDHoloCI queued the head commit of this pull request with the
That is a problem with the CI fleet, not a verdict on the code: nothing here says the change is right or wrong, and it needs another run before anyone relies on it. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(studio): P0b refuse .git landing paths + harden server git invocations
Base:
main@5a8f4ccb8dc8db4110596a3e3f9d060bc2d804de(#521, squash-merged) · Branch:grok/studio-p0b-git-hardening-20261005· Head:2871a406acProblem
lib/workspace/workspaceFs.tsblocked.gitby the request text only (segments[0] === '.git'). It never looked at where a path lands after symlinks.Release's re-gate of #521 at
51938b22reproduced the full chain with real git 2.47.3:gitalias -> .gitsurvivesgit clone(git checks links out as links).POST /api/workspace/fileswriteofgitalias/configpassed the text check and fix(studio): P0 cap exception — workspace owner check (IDOR on /api/workspace/*, /api/git/*, Brittney, daemon, holodaemon) #521's symlink-landing check (the landing.git/configis inside the workspace), returned 200, and overwrote.git/config.core.fsmonitorto a script there makes the next server-sidegit status(/api/git/status, and theexistingWorkspaceImportergit calls) run that script as the Studio user.core.hooksPath/.git/hooks/*do the same oncommit/checkout.Repro artifacts:
/workspace/security-4/regate-51938b22/(theOBSERVE .git alias write | 200line insymlink-controls.txt).Fix — two independent guards
Guard 1: refuse
.gitby resolved/landing path, not text (lib/workspace/workspaceFs.ts)hasGitMetadataSegment(relativePath): true when any path segment is.git, any depth, case-insensitive and tolerant of trailing dots/spaces (so.GIT/.git.on a case-folding or Windows FS count)..gitignore,my.git.txt,.github/andsrc/git/are not matched.resolveInsideWorkspacenow refuses (GIT_METADATA_ERROR) when any of these has a.gitsegment: the lexical target, the realpathed deepest-existing ancestor, or the B2 landing path (every symlink component followed). So agitalias -> .gitsymlink, a nesteda/b/.git/x, or an outside link that lands in a.gitare all refused before anything is written./api/workspace/fileslist/read/write/move/delete/mkdir (so reads too — it was free and safe, the resolver is shared),/api/workspace/paper-opt-in, anddaemon/jobsstore.applyPatchesToWorkspaceBranchpatch targets.validateWorkspaceRelativePathkeeps a cheap early text-level.gitrefusal (defence in depth)./api/git/treerefuses to list a.gitdirectory named directly or reached through a symlink.assertWorkspaceOwnerrefuses a workspace path that is or sits inside.git(so aprojectPath/rootPath/workspacePathcan't point at repo metadata).Guard 2: one hardened git helper (
lib/git/safeGit.ts)runGit(args, opts)/runGitSync(args, opts)prepend hardening config (command-line-coverrides repo, global and system config and is inherited by git's own subprocesses):core.fsmonitor=false— no fsmonitor hook program onstatus/diff/add/commit.core.hooksPath=/dev/null— not a directory, so no repo hook runs oncommit/checkout/clone/push.protocol.ext.allow=never—ext::remotes run an arbitrary command; the push/ship routes use the repo's own remote URL, so a repo config must not be able to re-enable it.Deliberately omitted, kept minimal (residuals):
GIT_CONFIG_NOSYSTEM: system config belongs to the image, not a tenant; disabling it could drop legitimate settings (e.g.safe.directory).core.sshCommand=: Studio talks to GitHub over HTTPS with a token; an empty value would only break SSH remotes. Like other repo-config program settings (credential.helper,filter/textconvdrivers,gpg.program), it requires a writable.git, which Guard 1 now closes. Called out below.Call-site table — every server
gitinvocationlib/git/safeGit?app/api/git/status/route.ts(status, rev-parse, log)execFile('git')runGitapp/api/git/branch/route.ts(branch, rev-parse)execFile('git')runGitapp/api/git/commit/route.ts(add, status, commit, rev-parse)execFile('git')runGitapp/api/git/diff/route.ts(diff)execFile('git')runGitapp/api/git/blame/route.ts(blame)execFile('git')runGitapp/api/git/push/route.ts(remote get/set-url)execFile('git')runGitapp/api/git/ship/route.ts(add, status, commit, rev-parse, remote)execFile('git')runGitapp/api/workspace/import/route.ts(clone, rev-parse, ls-files)execFile('git')runGit(via itsexecGit)app/api/daemon/jobs/store.ts(apply-to-branch: status, checkout, add, commit, remote…)execFileSync('git')runGitSynclib/workspace/existingWorkspaceImporter.ts(rev-parse, status)execFile('git')runGitlib/workspace/provisionUser.ts(read-onlygit -C)execFile('git')runGitlib/workspace/founderWorkspaceBackfill.ts(config --get remote.origin.url)execFile('git')runGitlib/workspace/templates/hooks.tsexecSync('git …')in a generated hook file's sourceapp/api/holoclaw/run,agents/fleet/dispatch,quest-proof/task,portable-mindspawn/spawnSyncof node/npx/tsx, never gitrgforexecFile/spawn/exec('git'acrosspackages/studio/src(excluding tests) now returns onlylib/git/safeGit.ts.Tests (real git in temp dirs)
lib/workspace/__tests__/gitHardening.p0b.test.ts(6) andlib/git/__tests__/safeGit.test.ts(2).5a8f4ccbgitalias -> .gitrefused;.git/configunchanged.git/configwrite refused (text + landing; read/move/mkdir via symlink too)core.fsmonitorin.git/confignot run by/api/git/status(marker absent)core.hooksPathpre-commit hook not run by/api/git/commit(marker absent).gitstill work (.gitignore,my.git.txt,.github/,src/git/)core.fsmonitorreads backfalseat runtime (real git)Red-on-base was measured by checking out the touched source files at
5a8f4ccband running the behavioral suite: 5 failed / 1 passed (the 1 is (e), which always held). Log:p0b-behavioral-on-base.log.Negative controls (remove a guard → red; restore → green)
.gitlanding guard neutralized (hasGitMetadataSegment -> false)GIT_HARDENING_ARGS -> [])The full-chain end-to-end control goes red under either guard removal, as required. Script + JSON:
p0b-negative-controls.py,p0b-negative-controls.json.Suite results (same box, same env)
5a8f4ccblib/workspace,api/workspace,api/git,api/daemon,lib/daemon,api/holodaemon,api/brittney,lib/brittney,lib/absorb,lib/git, plus fix(studio): P0 cap exception — invite-only sign-in (founder/admin allowlist, fail closed) #516 auth suites): 862 passed; 6 files fail to load on unbuilt@holoscript/*packages, all failing on main too.src/__tests__/studio-docker-runtime-tools.test.tsasserted the import route containedexecFile('git'. git now runs vialib/git/safeGit, so it now asserts the route imports@/lib/git/safeGitand that the helper is whereexecFile('git')/execFileSync('git')live — same invariant (the serving image still needs git).tsc --noEmit: 399error TSat this head and 399 at5a8f4ccb, no new errors in any touched file (only pre-existing TS2307 for unbuilt packages). Prettier clean on all touched files.Residuals
credential.helper,filter/textconvdrivers,gpg.program,core.sshCommand,core.pager) are not each pinned on the command line. They all require a writable.git, which Guard 1 now denies, so they are not reachable through the known write path; pinning them (orGIT_CONFIG_NOSYSTEM/core.sshCommand=) is a possible hardening follow-up, left out to stay minimal.lib/workspace/templates/hooks.ts) run git in the user's own checkout outside Studio's server process; unchanged here.5a8f4ccbbefore merge.