Skip to content

Commit 0363885

Browse files
committed
fix: CodeQL batch — secure temps, unbiased IDs, loopback inspect URLs
Use mkdtemp for doctor/config/reload/tower fixtures. Unbiased task-id alphabet sampling. Strict Vertex host parse. Cache-dir originals with O_EXCL write. Config domain allowlist. Inspect joinApiUrl + loopback-only base/WS. CodeQL paths-ignore for committed dist-web vendor bundles.
1 parent 5f3cc2f commit 0363885

32 files changed

Lines changed: 204 additions & 124 deletions

File tree

‎.github/workflows/codeql.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ jobs:
2626
build-mode: none
2727
languages: javascript-typescript
2828
queries: security-extended
29+
config: |
30+
paths-ignore:
31+
- apps/pythinker-code/dist-web/**
32+
- '**/dist-web/**'
33+
- '**/*.min.js'
2934
- uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4
3035
with:
3136
category: /language:javascript-typescript

‎apps/pythinker-code/scripts/native/exec.mjs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@ import { appRoot } from './paths.mjs';
66

77
const execFileAsync = promisify(execFile);
88

9-
export function commandForExecFile(command, args, platform = process.platform, env = process.env) {
9+
export function commandForExecFile(command, args, platform = process.platform, _env = process.env) {
1010
if (platform !== 'win32' || !/\.(?:bat|cmd)$/i.test(command)) {
1111
return { command, args };
1212
}
13+
// Fixed shell binary — never take ComSpec from the environment (CodeQL).
1314
const shellCommand = [command, ...args]
1415
.map((arg) => `"${String(arg).replaceAll('"', '""')}"`)
1516
.join(' ');
1617
return {
17-
command: env.ComSpec ?? 'cmd.exe',
18+
command: 'cmd.exe',
1819
args: ['/d', '/s', '/c', `"${shellCommand}"`],
1920
options: { windowsVerbatimArguments: true },
2021
};

‎apps/pythinker-code/src/tui/utils/image-placeholder.ts‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141

4242
import { createHash, randomUUID } from 'node:crypto';
4343
import { copyFileSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
44-
import { tmpdir } from 'node:os';
4544
import { join } from 'node:path';
4645

4746
import type { PromptPart, Session } from '@pymodel/pythinker-code-sdk';
@@ -664,15 +663,19 @@ export function persistOriginalImageSync(
664663
const hash = createHash('sha256').update(bytes).digest('hex').slice(0, 32);
665664
const target = join(targetDir, `${hash}.${imageExtensionForMime(mime)}`);
666665
mkdirSync(targetDir, { recursive: true });
667-
const existing = statSync(target, { throwIfNoEntry: false });
668-
// Content-addressed: an existing entry with the right size IS this image.
669-
if (existing === undefined || existing.size !== bytes.length) {
670-
writeFileSync(target, bytes);
666+
try {
667+
writeFileSync(target, bytes, { flag: 'wx' });
668+
} catch (error) {
669+
const code = error instanceof Error && 'code' in error ? (error as NodeJS.ErrnoException).code : undefined;
670+
if (code !== 'EEXIST') throw error;
671671
}
672672
sweepCacheSync(targetDir, maxTotalBytes);
673-
// The just-written file may itself have been evicted by the sweep when a
674-
// single original exceeds the cap; report persistence honestly.
675-
return statSync(target, { throwIfNoEntry: false }) === undefined ? null : target;
673+
try {
674+
const existing = statSync(target);
675+
return existing.isFile() && existing.size === bytes.length ? target : null;
676+
} catch {
677+
return null;
678+
}
676679
} catch {
677680
return null;
678681
}
@@ -706,7 +709,7 @@ function sweepCacheSync(dir: string, maxTotalBytes: number): void {
706709

707710
/** Mirrors agent-core-v2's `originalImageCacheDir` (not re-exported through the SDK). */
708711
function originalImageTempDir(): string {
709-
return join(tmpdir(), 'pythinker-code-original-images');
712+
return join(getCacheDir(), 'original-images');
710713
}
711714

712715
/**

‎apps/pythinker-code/src/utils/clipboard/clipboard-image.ts‎

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,6 @@ function splitClipboardPathLines(text: string): string[] {
172172
}
173173

174174
function readImagePath(path: string): ClipboardImage | null {
175-
let stat: ReturnType<typeof statSync>;
176-
try {
177-
stat = statSync(path);
178-
} catch {
179-
return null;
180-
}
181-
if (!stat.isFile()) return null;
182-
183175
let bytes: Buffer;
184176
try {
185177
bytes = readFileSync(path);

‎apps/pythinker-code/test/cli/doctor.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, rm, writeFile } from 'node:fs/promises';
1+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
22
import { tmpdir } from 'node:os';
33
import { join, resolve } from 'node:path';
44

@@ -14,8 +14,7 @@ import {
1414
let dir: string;
1515

1616
beforeEach(async () => {
17-
dir = join(tmpdir(), `pythinker-doctor-${Date.now()}-${Math.random().toString(36).slice(2)}`);
18-
await mkdir(dir, { recursive: true });
17+
dir = await mkdtemp(join(tmpdir(), 'pythinker-doctor-'));
1918
});
2019

2120
afterEach(async () => {

‎apps/pythinker-code/test/cli/login.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ describe('pythinker login', () => {
117117

118118
const writtenChunks = stderrSpy.mock.calls.map((call: unknown[]) => String(call[0]));
119119
expect(writtenChunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true);
120-
expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe(
120+
expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v?code=ABCD-EFGH') || /https:\/\/example\.com\/v(?:\s|$)/.test(chunk))).toBe(
121121
true,
122122
);
123123
expect(openUrl).toHaveBeenCalledWith('https://example.com/v?code=ABCD-EFGH');
@@ -157,7 +157,7 @@ describe('pythinker login', () => {
157157

158158
const writtenChunks = stderrSpy.mock.calls.map((call: unknown[]) => String(call[0]));
159159
expect(writtenChunks.some((chunk: string) => chunk.includes('ABCD-EFGH'))).toBe(true);
160-
expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v'))).toBe(
160+
expect(writtenChunks.some((chunk: string) => chunk.includes('https://example.com/v?code=ABCD-EFGH') || /https:\/\/example\.com\/v(?:\s|$)/.test(chunk))).toBe(
161161
true,
162162
);
163163
expect(openUrl).toHaveBeenCalledWith('https://example.com/v?code=ABCD-EFGH');

‎apps/pythinker-code/test/tui/commands/reload.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdir, rm, writeFile } from 'node:fs/promises';
1+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

@@ -195,9 +195,8 @@ auto_install = false
195195
});
196196

197197
async function writeTuiConfig(text: string): Promise<void> {
198-
const dir = join(tmpdir(), `pythinker-tui-reload-${Date.now()}-${Math.random().toString(36).slice(2)}`);
198+
const dir = await mkdtemp(join(tmpdir(), 'pythinker-tui-reload-'));
199199
tempDirs.push(dir);
200-
await mkdir(dir, { recursive: true });
201200
process.env['PYTHINKER_CODE_HOME'] = dir;
202201
await writeFile(join(dir, 'tui.toml'), text, 'utf-8');
203202
}

‎apps/pythinker-code/test/tui/config.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
1+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44

@@ -17,8 +17,7 @@ let dir: string;
1717
let filePath: string;
1818

1919
beforeEach(() => {
20-
dir = join(tmpdir(), `pythinker-tui-config-${Date.now()}-${Math.random().toString(36).slice(2)}`);
21-
mkdirSync(dir, { recursive: true });
20+
dir = mkdtempSync(join(tmpdir(), 'pythinker-tui-config-'));
2221
filePath = join(dir, 'tui.toml');
2322
});
2423

‎apps/pythinker-inspect/src/activity/store.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import type { WsLikeCtor } from '../channel/wsLike';
1818
import { GlobalEventsWs, type SessionWorkFacts } from './ws';
19+
import { joinApiUrl } from '../httpUrl';
1920

2021
export type { SessionWorkFacts };
2122

@@ -126,7 +127,7 @@ export class SessionActivityHub {
126127
headers['authorization'] = `Bearer ${this.token}`;
127128
}
128129
try {
129-
const res = await this.fetchImpl(`${this.baseUrl}/api/v1/sessions`, { headers });
130+
const res = await this.fetchImpl(joinApiUrl(this.baseUrl, '/api/v1/sessions'), { headers });
130131
const envelope = (await res.json()) as {
131132
code: number;
132133
data?: { items?: Record<string, unknown>[] };

‎apps/pythinker-inspect/src/activity/ws.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,10 @@ function toWsUrl(base: string): string {
299299
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') {
300300
throw new Error(`unsupported URL scheme for WS transport: ${base}`);
301301
}
302+
const host = url.hostname.replaceAll(/^\[|\]$/g, '').toLowerCase();
303+
if (host !== 'localhost' && host !== '127.0.0.1' && host !== '::1') {
304+
throw new Error(`WS transport must target loopback: ${base}`);
305+
}
302306
if (!url.pathname.endsWith('/api/v1/ws')) {
303307
url.pathname = `${url.pathname.replace(/\/$/, '')}/api/v1/ws`;
304308
}

0 commit comments

Comments
 (0)