Skip to content

Commit aeaee59

Browse files
improv: validation
1 parent 27c559d commit aeaee59

File tree

2 files changed

+99
-8
lines changed

2 files changed

+99
-8
lines changed

core/util/sanitization.vitest.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { execSync } from "child_process";
22
import { describe, expect, it } from "vitest";
3+
import { normalizeRepoUrl } from "./repoUrl";
34
import { sanitizeShellArgument, validateGitHubRepoUrl } from "./sanitization";
45

56
describe("sanitizeShellArgument", () => {
@@ -87,6 +88,81 @@ describe("validateGitHubRepoUrl", () => {
8788
expect(validateGitHubRepoUrl(null as any)).toBe(false);
8889
expect(validateGitHubRepoUrl(undefined as any)).toBe(false);
8990
});
91+
92+
describe("validation after normalization", () => {
93+
it("should validate normalized URLs to prevent bypass", () => {
94+
// Test that validation works on normalized output
95+
const inputs = [
96+
"owner/repo",
97+
"[email protected]:owner/repo.git",
98+
"https://github.com/owner/repo.git",
99+
"ssh://[email protected]/owner/repo.git",
100+
];
101+
102+
inputs.forEach((input) => {
103+
const normalized = normalizeRepoUrl(input);
104+
expect(validateGitHubRepoUrl(normalized)).toBe(true);
105+
});
106+
});
107+
108+
it("should catch dangerous URLs even after normalization", () => {
109+
// These should still be dangerous after normalization
110+
const dangerous = [
111+
"owner/repo; rm -rf /",
112+
"owner/repo && malicious",
113+
"owner/repo | cat /etc/passwd",
114+
];
115+
116+
dangerous.forEach((input) => {
117+
// Should be blocked before normalization
118+
expect(validateGitHubRepoUrl(input)).toBe(false);
119+
120+
// Even if somehow normalized, should still be invalid
121+
const normalized = normalizeRepoUrl(input);
122+
expect(validateGitHubRepoUrl(normalized)).toBe(false);
123+
});
124+
});
125+
126+
it("should handle edge cases where normalization changes URL structure", () => {
127+
// Test URLs that change during normalization
128+
const testCases = [
129+
{
130+
input: "Owner/Repo.git/",
131+
normalized: "https://github.com/owner/repo",
132+
shouldBeValid: true,
133+
},
134+
{
135+
input: "[email protected]:owner/repo.git",
136+
normalized: "https://github.com/owner/repo",
137+
shouldBeValid: true,
138+
},
139+
];
140+
141+
testCases.forEach(({ input, normalized, shouldBeValid }) => {
142+
const actualNormalized = normalizeRepoUrl(input);
143+
expect(actualNormalized).toBe(normalized);
144+
expect(validateGitHubRepoUrl(actualNormalized)).toBe(shouldBeValid);
145+
});
146+
});
147+
148+
it("should prevent validation bypass via URL encoding or special chars", () => {
149+
// These tests ensure that validation happens AFTER normalization
150+
// preventing attackers from bypassing validation via encoding or transformation
151+
152+
// Currently validateGitHubRepoUrl blocks these, but this test ensures
153+
// the pattern of "normalize then validate" is maintained
154+
const potentialBypass = [
155+
"../../../etc/passwd",
156+
"owner/../malicious",
157+
"owner/repo`whoami`",
158+
"owner/repo$(whoami)",
159+
];
160+
161+
potentialBypass.forEach((input) => {
162+
expect(validateGitHubRepoUrl(input)).toBe(false);
163+
});
164+
});
165+
});
90166
});
91167

92168
/**

extensions/vscode/src/extension/VsCodeMessenger.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -300,19 +300,19 @@ export class VsCodeMessenger {
300300
// Get repo name/URL
301301
const repoName = await this.ide.getRepoName(workspaceDir);
302302
if (repoName) {
303-
// Validate repo URL to prevent injection attacks
304-
if (!validateGitHubRepoUrl(repoName)) {
303+
// Normalize the URL first to get canonical form
304+
const normalized = normalizeRepoUrl(repoName);
305+
306+
// Validate the normalized URL to prevent injection attacks
307+
// This ensures we validate what we'll actually use, not just the input
308+
if (!validateGitHubRepoUrl(normalized)) {
305309
vscode.window.showErrorMessage(
306310
"Invalid repository format. Please ensure you're using a valid GitHub repository.",
307311
);
308312
return;
309313
}
310-
// If repo name looks like "owner/repo", convert to GitHub URL
311-
if (repoName.includes("/") && !repoName.startsWith("http")) {
312-
repoUrl = `https://github.com/${repoName}`;
313-
} else {
314-
repoUrl = repoName;
315-
}
314+
315+
repoUrl = normalized;
316316
}
317317

318318
// Get current branch
@@ -464,14 +464,29 @@ export class VsCodeMessenger {
464464
return;
465465
}
466466

467+
// Validate the repo URL from API response to prevent injection attacks
468+
if (!validateGitHubRepoUrl(repoUrl)) {
469+
vscode.window.showErrorMessage(
470+
"Invalid repository URL from agent session. Please contact support.",
471+
);
472+
return;
473+
}
474+
467475
// Get workspace directories
468476
const workspaceDirs = await this.ide.getWorkspaceDirs();
469477
if (workspaceDirs.length === 0) {
470478
vscode.window.showErrorMessage("No workspace folder is open.");
471479
return;
472480
}
473481

482+
// Normalize and validate again to ensure the normalized form is safe
474483
const normalizedAgentRepo = normalizeRepoUrl(repoUrl);
484+
if (!validateGitHubRepoUrl(normalizedAgentRepo)) {
485+
vscode.window.showErrorMessage(
486+
"Invalid repository URL after normalization. Please contact support.",
487+
);
488+
return;
489+
}
475490

476491
// Find the workspace that matches the agent's repo URL
477492
let matchingWorkspace: string | null = null;

0 commit comments

Comments
 (0)