|
1 | 1 | import { execSync } from "child_process"; |
2 | 2 | import { describe, expect, it } from "vitest"; |
| 3 | +import { normalizeRepoUrl } from "./repoUrl"; |
3 | 4 | import { sanitizeShellArgument, validateGitHubRepoUrl } from "./sanitization"; |
4 | 5 |
|
5 | 6 | describe("sanitizeShellArgument", () => { |
@@ -87,6 +88,81 @@ describe("validateGitHubRepoUrl", () => { |
87 | 88 | expect(validateGitHubRepoUrl(null as any)).toBe(false); |
88 | 89 | expect(validateGitHubRepoUrl(undefined as any)).toBe(false); |
89 | 90 | }); |
| 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 | + }); |
90 | 166 | }); |
91 | 167 |
|
92 | 168 | /** |
|
0 commit comments