Repository navigation
feat(scripts): workspace-misroute guard (fail loud in the wrong checkout) — #679 D3 #680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||||||||||||||
| #!/usr/bin/env node | ||||||||||||||||||
| 'use strict'; | ||||||||||||||||||
| /** | ||||||||||||||||||
| * Refuse repo-specific work in the WRONG checkout — a durable guard for the launcher-misrouting defect | ||||||||||||||||||
| * (DECISION 3, #679 sprint). The session launcher has bound a TrustShell task to the old `ihh-ica` project | ||||||||||||||||||
| * while the working dir was the `trustshell-tl` worktree, so `start_session` resolved the wrong repo and | ||||||||||||||||||
| * work risked landing (or silently failing) against the wrong history. The session-binding itself is | ||||||||||||||||||
| * app-level config this script cannot change; this is the in-repo assertion so any loop or session can gate | ||||||||||||||||||
| * its own work and fail LOUDLY instead of silently misrouting: | ||||||||||||||||||
| * | ||||||||||||||||||
| * node scripts/check-workspace.cjs # expects origin = DealAppSeo/trustshell | ||||||||||||||||||
| * node scripts/check-workspace.cjs owner/repo # expects that repo | ||||||||||||||||||
| * TRUSTSHELL_EXPECT_REPO=owner/repo node scripts/check-workspace.cjs | ||||||||||||||||||
| * | ||||||||||||||||||
| * Exit 0 when origin matches, 1 (with a loud message) otherwise. No deps; safe to call at task startup. | ||||||||||||||||||
| */ | ||||||||||||||||||
| const { execSync } = require('node:child_process'); | ||||||||||||||||||
|
|
||||||||||||||||||
| /** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */ | ||||||||||||||||||
| function workspaceMatches(remoteUrl, expected) { | ||||||||||||||||||
| const m = String(remoteUrl) | ||||||||||||||||||
| .trim() | ||||||||||||||||||
| .match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i); | ||||||||||||||||||
| return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| /** Resolve the current origin + branch and assert it is `expected`. Returns the process exit code. */ | ||||||||||||||||||
| function main(argv, env, run) { | ||||||||||||||||||
| const expected = argv[2] || env.TRUSTSHELL_EXPECT_REPO || 'DealAppSeo/trustshell'; | ||||||||||||||||||
| const sh = run || ((cmd) => execSync(cmd, { encoding: 'utf8' }).trim()); | ||||||||||||||||||
| let remote = ''; | ||||||||||||||||||
| let branch = ''; | ||||||||||||||||||
| try { remote = sh('git remote get-url origin'); } catch { /* no remote */ } | ||||||||||||||||||
| try { branch = sh('git rev-parse --abbrev-ref HEAD'); } catch { /* detached / no repo */ } | ||||||||||||||||||
| if (!workspaceMatches(remote, expected)) { | ||||||||||||||||||
| process.stderr.write( | ||||||||||||||||||
| `WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` + | ||||||||||||||||||
| `Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`, | ||||||||||||||||||
| ); | ||||||||||||||||||
|
Comment on lines
+36
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟢 Workspace guard prints credential-bearing git remote URL verbatim to stderrSeverity: LOW · CWE-532 The new workspace guard writes the raw output of
Suggested change
Prompt to fix with AIThis is a security vulnerability found during a code review.
Vulnerability: Workspace guard prints credential-bearing git remote URL verbatim to stderr
Severity: LOW
CWE: CWE-532
The new workspace guard writes the raw output of `git remote get-url origin` into its failure message, so any credentials embedded in the remote URL are disclosed in full. When a checkout is cloned with a token in the URL — the common `https://x-access-token:$TOKEN@github.com/owner/repo.git` form used by CI runners and agent sandboxes — and the guard fires on a misroute, the token is emitted to stderr, which is captured in terminal scrollback, CI job logs, and agent session transcripts that are read by more parties than the local git config. The script exists precisely to run at task startup in automated sessions, and the misroute path is the one that fires in misconfigured environments, so the leak lands in exactly the logs that get shared and retained. The repository's other tooling already redacts secrets and strips tokens before logging, so this path is inconsistent with the project's own hygiene standard.
Location: scripts/check-workspace.cjs:36-39
Context: Failure message echoes the raw remote URL, including any embedded credentials
```
// Before:
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${remote || '(none)'}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);
// After:
process.stderr.write(
`WORKSPACE MISROUTED: origin is "${redactRemote(remote)}", expected ${expected}. ` +
`Refusing repo-specific work in the wrong checkout — cd to the ${expected} worktree first.\n`,
);
```
Location: scripts/check-workspace.cjs:19-25
Context: Adds the redaction helper used by the failure message
```
// Before:
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
// After:
/** PURE. Does a git remote URL (https or ssh, optional `.git` / trailing slash) point at `owner/repo`? */
function workspaceMatches(remoteUrl, expected) {
const m = String(remoteUrl)
.trim()
.match(/github\.com[/:]([^/\s]+\/[^/\s]+?)(?:\.git)?\/?$/i);
return m ? m[1].toLowerCase() === String(expected).toLowerCase() : false;
}
/** Strip any credentials embedded in a remote URL before it is written to a log. */
function redactRemote(remoteUrl) {
const s = String(remoteUrl || '').trim();
return s ? s.replace(/\/\/[^/@\s]+@/, '//<redacted>@') : '(none)';
}
```
How to fix:
Redact the userinfo component of the remote URL before it is written to stderr, or print only the parsed `owner/repo` (which is all the message needs to be actionable). Apply the same redaction to any other diagnostic that echoes a remote URL, and consider a shared helper so future scripts cannot reintroduce the pattern.
Please fix this vulnerability. If you propose a fix, make it concise and minimal.React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding. |
||||||||||||||||||
| return 1; | ||||||||||||||||||
| } | ||||||||||||||||||
| process.stderr.write(`workspace OK: ${expected}${branch ? ` @ ${branch}` : ''}\n`); | ||||||||||||||||||
| return 0; | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| module.exports = { workspaceMatches, main }; | ||||||||||||||||||
|
|
||||||||||||||||||
| if (require.main === module) process.exit(main(process.argv, process.env)); | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| /** | ||
| * The workspace guard refuses repo-specific work in the wrong checkout (DECISION 3, #679 sprint): the launcher | ||
| * bound a TrustShell task to the old ihh-ica project. `workspaceMatches` is pure; `main` is run with an injected | ||
| * shell so the test asserts both exit paths without depending on the checkout it runs in. | ||
| */ | ||
| const { workspaceMatches, main } = require('../scripts/check-workspace.cjs') as { | ||
| workspaceMatches: (remoteUrl: string, expected: string) => boolean; | ||
| main: (argv: string[], env: NodeJS.ProcessEnv, run?: (cmd: string) => string) => number; | ||
| }; | ||
|
|
||
| describe('check-workspace: refuse the wrong checkout (DECISION 3, #679)', () => { | ||
| it('matches https and ssh forms of the expected repo, with or without .git / case / trailing slash', () => { | ||
| for (const u of [ | ||
| 'https://github.com/DealAppSeo/trustshell.git', | ||
| 'https://github.com/DealAppSeo/trustshell', | ||
| 'git@github.com:DealAppSeo/trustshell.git', | ||
| 'https://github.com/dealappseo/TrustShell/', | ||
| ]) { | ||
| expect(workspaceMatches(u, 'DealAppSeo/trustshell')).toBe(true); | ||
| } | ||
| }); | ||
|
|
||
| it('rejects the ihh-ica misroute, a different repo, and empty/garbage remotes', () => { | ||
| expect(workspaceMatches('https://github.com/DealAppSeo/ihh-ica.git', 'DealAppSeo/trustshell')).toBe(false); | ||
| expect(workspaceMatches('git@github.com:DealAppSeo/repid-engine.git', 'DealAppSeo/trustshell')).toBe(false); | ||
| expect(workspaceMatches('', 'DealAppSeo/trustshell')).toBe(false); | ||
| expect(workspaceMatches('not-a-url', 'DealAppSeo/trustshell')).toBe(false); | ||
| }); | ||
|
|
||
| it('main() exits 0 on a matching origin and 1 (loudly) on a misroute — injected shell, no real git', () => { | ||
| const asTrustshell = (cmd: string) => (cmd.includes('remote get-url') ? 'https://github.com/DealAppSeo/trustshell.git' : 'main'); | ||
| const asIhhIca = (cmd: string) => (cmd.includes('remote get-url') ? 'https://github.com/DealAppSeo/ihh-ica.git' : 'ica-build'); | ||
| expect(main(['node', 'check-workspace.cjs'], {} as NodeJS.ProcessEnv, asTrustshell)).toBe(0); | ||
| expect(main(['node', 'check-workspace.cjs'], {} as NodeJS.ProcessEnv, asIhhIca)).toBe(1); | ||
| // explicit expected-repo arg wins | ||
| expect(main(['node', 'check-workspace.cjs', 'DealAppSeo/repid-engine'], {} as NodeJS.ProcessEnv, (cmd) => (cmd.includes('remote get-url') ? 'git@github.com:DealAppSeo/repid-engine.git' : 'main'))).toBe(0); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adds the redaction helper used by the failure message