Skip to content

Commit 43fc6aa

Browse files
abhiaiyer91Mastra Code (anthropic/claude-fable-5)
andcommitted
fix(mastracode-web): validate setup commands and document their trust model
Address the security-scan finding on setupCommand execution: reject control characters (except newline/tab) at the settings route so escape sequences and NULs can't reach the sandbox shell or spoof logs, and document why arbitrary shell is intentional — the command is only configurable by authenticated org members and runs exclusively inside the project's isolated sandbox, the same environment where org members already run arbitrary shell through the agent. Co-Authored-By: Mastra Code (anthropic/claude-fable-5) <noreply@mastra.ai>
1 parent 64b2e86 commit 43fc6aa

3 files changed

Lines changed: 32 additions & 0 deletions

File tree

mastracode/web/src/web/github/routes.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,22 @@ describe('project settings routes', () => {
918918
});
919919
expect(res.status).toBe(400);
920920
});
921+
922+
it('400s on a setup command containing control characters', async () => {
923+
seedMaterializedProject();
924+
const app = buildApp({ workosId: 'u1' });
925+
const res = await postJson(app, '/web/github/projects/p1/settings', {
926+
setupCommand: 'pnpm i \x1b[31m&& rm -rf /',
927+
});
928+
expect(res.status).toBe(400);
929+
expect(tables.projects[0].setupCommand).toBeNull();
930+
931+
// Newlines and tabs are legitimate in multi-line setup scripts.
932+
const res2 = await postJson(app, '/web/github/projects/p1/settings', {
933+
setupCommand: 'pnpm i\npnpm build\t--force',
934+
});
935+
expect(res2.status).toBe(200);
936+
});
921937
});
922938

923939
describe('worktree route', () => {

mastracode/web/src/web/github/routes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,12 @@ export function buildGithubRoutes(options: MountGithubRoutesOptions = {}): ApiRo
592592
if (typeof body.setupCommand === 'string' && body.setupCommand.length > 2000) {
593593
return c.json({ error: 'setupCommand too long (max 2000 characters)' }, 400);
594594
}
595+
// Reject control characters (except newline/tab). The command is a
596+
// shell script by design, but escape sequences and NULs have no
597+
// legitimate use and can spoof logs or confuse the sandbox shell.
598+
if (typeof body.setupCommand === 'string' && /[\0-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(body.setupCommand)) {
599+
return c.json({ error: 'setupCommand contains control characters' }, 400);
600+
}
595601
// An empty/whitespace command means "no setup step".
596602
const setupCommand =
597603
typeof body.setupCommand === 'string' && body.setupCommand.trim().length > 0

mastracode/web/src/web/github/sandbox.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,16 @@ export async function ensureWorktree(
845845
* run so the checkout is ready to build/test. A non-zero exit is a hard error —
846846
* starting agent work in a half-set-up tree is worse than failing the request.
847847
*
848+
* Security model: the command is intentionally arbitrary shell — that is the
849+
* feature (install deps, build, seed fixtures). It is only configurable by
850+
* authenticated org members (the settings route is gated by
851+
* `resolveOrgTenant` + org-scoped project lookup, with length and
852+
* control-character validation), and it executes exclusively inside the
853+
* project's isolated sandbox — the same environment where org members already
854+
* run arbitrary shell via the agent's command tool. It never runs on the web
855+
* server host, so it grants no privilege beyond what sandbox access already
856+
* provides.
857+
*
848858
* @param sandbox live sandbox containing the worktree
849859
* @param worktreePath the server-computed worktree path the command runs in
850860
* @param command the org-configured setup shell command

0 commit comments

Comments
 (0)