Skip to content

Commit c990c6b

Browse files
MossakaclaudeCopilot
authored
fix: prevent Squid config injection via --allow-domains and --allow-urls (#1517)
* fix: prevent Squid config injection via domain inputs Malicious input to --allow-domains or --allow-urls containing whitespace, newlines, or null bytes could inject arbitrary directives into the generated squid.conf. This adds: 1. Character validation in validateDomainOrPattern() rejects whitespace, null bytes, quotes, semicolons, backslashes 2. assertSafeForSquidConfig() in squid-config.ts validates every value before interpolation into squid.conf 3. URL pattern validation in cli.ts for --allow-urls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use JSON.stringify in error message to prevent log injection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use shared SQUID_DANGEROUS_CHARS for URL/domain validation Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/62b8aef1-1409-4f1d-8641-71dce77c2ceb * fix: correct error message for URL pattern validation Agent-Logs-Url: https://github.com/github/gh-aw-firewall/sessions/62b8aef1-1409-4f1d-8641-71dce77c2ceb --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 5ed84e4 commit c990c6b

File tree

5 files changed

+188
-2
lines changed

5 files changed

+188
-2
lines changed

src/cli.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from './host-iptables';
2323
import { runMainWorkflow } from './cli-workflow';
2424
import { redactSecrets } from './redact-secrets';
25-
import { validateDomainOrPattern } from './domain-patterns';
25+
import { validateDomainOrPattern, SQUID_DANGEROUS_CHARS } from './domain-patterns';
2626
import { loadAndMergeDomains } from './rules';
2727
import { detectHostDnsServers } from './dns-resolver';
2828
import { OutputFormat } from './types';
@@ -1666,6 +1666,13 @@ program
16661666
}
16671667
}
16681668

1669+
// Reject characters that could inject Squid config directives or tokens
1670+
if (SQUID_DANGEROUS_CHARS.test(url)) {
1671+
logger.error(`URL pattern contains characters unsafe for Squid config: ${JSON.stringify(url)}`);
1672+
logger.error('URL patterns must not contain whitespace, quotes, semicolons, backticks, hash characters, or null bytes.');
1673+
process.exit(1);
1674+
}
1675+
16691676
// Ensure pattern has a path component (not just domain)
16701677
const urlWithoutScheme = url.replace(/^https:\/\//, '');
16711678
if (!urlWithoutScheme.includes('/')) {

src/domain-patterns.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,70 @@ describe('validateDomainOrPattern', () => {
232232
});
233233
});
234234

235+
describe('rejects injection characters', () => {
236+
it('should reject LF in domain', () => {
237+
expect(() => validateDomainOrPattern('evil.com\nhttp_access allow all')).toThrow('contains invalid character');
238+
});
239+
240+
it('should reject CR in domain', () => {
241+
expect(() => validateDomainOrPattern('evil.com\rhttp_access allow all')).toThrow('contains invalid character');
242+
});
243+
244+
it('should reject CRLF in domain', () => {
245+
expect(() => validateDomainOrPattern('evil.com\r\nhttp_access allow all')).toThrow('contains invalid character');
246+
});
247+
248+
it('should reject null bytes', () => {
249+
expect(() => validateDomainOrPattern('evil.com\0')).toThrow('contains invalid character');
250+
});
251+
252+
it('should reject tabs', () => {
253+
expect(() => validateDomainOrPattern('evil.com\tallowed')).toThrow('contains invalid character');
254+
});
255+
256+
it('should reject interior spaces', () => {
257+
expect(() => validateDomainOrPattern('evil.com allowed')).toThrow('contains invalid character');
258+
});
259+
260+
it('should reject space-separated domains (ACL token injection)', () => {
261+
expect(() => validateDomainOrPattern('.evil.com .attacker.com')).toThrow('contains invalid character');
262+
});
263+
264+
it('should reject semicolons', () => {
265+
expect(() => validateDomainOrPattern('evil.com;rm -rf')).toThrow('contains invalid character');
266+
});
267+
268+
it('should reject hash characters', () => {
269+
expect(() => validateDomainOrPattern('evil.com#comment')).toThrow('contains invalid character');
270+
});
271+
272+
it('should reject backslashes', () => {
273+
expect(() => validateDomainOrPattern('evil.com\\n')).toThrow('contains invalid character');
274+
});
275+
276+
it('should reject single quotes', () => {
277+
expect(() => validateDomainOrPattern("evil.com'")).toThrow('contains invalid character');
278+
});
279+
280+
it('should reject double quotes', () => {
281+
expect(() => validateDomainOrPattern('evil.com"')).toThrow('contains invalid character');
282+
});
283+
});
284+
285+
describe('accepts valid DNS names with underscores', () => {
286+
it('should accept _dmarc.example.com', () => {
287+
expect(() => validateDomainOrPattern('_dmarc.example.com')).not.toThrow();
288+
});
289+
290+
it('should accept _acme-challenge.example.com', () => {
291+
expect(() => validateDomainOrPattern('_acme-challenge.example.com')).not.toThrow();
292+
});
293+
294+
it('should accept _srv._tcp.example.com', () => {
295+
expect(() => validateDomainOrPattern('_srv._tcp.example.com')).not.toThrow();
296+
});
297+
});
298+
235299
describe('protocol-prefixed domains', () => {
236300
it('should accept valid http:// prefixed domains', () => {
237301
expect(() => validateDomainOrPattern('http://github.com')).not.toThrow();

src/domain-patterns.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@
1212
* github.com -> allow both HTTP and HTTPS (default)
1313
*/
1414

15+
/**
16+
* Characters that are dangerous in Squid config files when interpolating domain names
17+
* or URL regex patterns. Squid config is line-and-space delimited, so:
18+
* - Whitespace (space, tab, CR, LF) can split ACL tokens or inject new directives
19+
* - Null bytes may terminate strings unexpectedly
20+
* - `#` starts a Squid config comment, truncating the rest of the line
21+
* - Quotes (", ', `) and `;` can interfere with config parsing
22+
*
23+
* Note: backslash is intentionally excluded here because URL regex patterns passed to
24+
* `--allow-urls` legitimately use `\` for regex escaping (e.g., `\\.` or `[^\\s]`).
25+
* Domain names are additionally validated to reject `\` in validateDomainOrPattern().
26+
*/
27+
export const SQUID_DANGEROUS_CHARS = /[\s\0"'`;#]/;
28+
1529
/**
1630
* Protocol restriction for a domain
1731
*/
@@ -151,6 +165,24 @@ export function validateDomainOrPattern(input: string): void {
151165
throw new Error('Domain cannot be empty');
152166
}
153167

168+
// Reject characters that could inject Squid config directives or tokens.
169+
// Also reject backslash: domain names never legitimately contain backslashes,
170+
// and they could be used in regex injection if they reach Squid config.
171+
// This prevents Squid config injection via --allow-domains.
172+
const DOMAIN_DANGEROUS_CHARS = /[\s\0"'`;#\\]/;
173+
const match = trimmed.match(DOMAIN_DANGEROUS_CHARS);
174+
if (match) {
175+
const safeDomainForMessage = JSON.stringify(trimmed);
176+
const charCode = match[0].charCodeAt(0);
177+
const charDesc = charCode <= 0x20 || charCode === 0x7f
178+
? `U+${charCode.toString(16).padStart(4, '0')}`
179+
: `'${match[0]}'`;
180+
throw new Error(
181+
`Invalid domain ${safeDomainForMessage}: contains invalid character ${charDesc}. ` +
182+
`Domain names must not contain whitespace, quotes, semicolons, backticks, hash characters, backslashes, or control characters.`
183+
);
184+
}
185+
154186
// Check for overly broad patterns
155187
if (trimmed === '*') {
156188
throw new Error("Pattern '*' matches all domains and is not allowed");

src/squid-config.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,72 @@
11
import { generateSquidConfig, generatePolicyManifest } from './squid-config';
22
import { SquidConfig } from './types';
33

4+
describe('defense-in-depth: rejects injected values', () => {
5+
const defaultPort = 3128;
6+
7+
it('should reject newline in domain via validateDomainOrPattern', () => {
8+
expect(() => {
9+
generateSquidConfig({
10+
domains: ['evil.com\nhttp_access allow all'],
11+
port: defaultPort,
12+
});
13+
}).toThrow();
14+
});
15+
16+
it('should reject newline in URL pattern', () => {
17+
// URL patterns go through generateSslBumpSection, which interpolates into squid.conf.
18+
// The assertSafeForSquidConfig guard should catch this.
19+
const maliciousPattern = 'https://evil.com/path\nhttp_access allow all';
20+
expect(() => {
21+
generateSquidConfig({
22+
domains: ['evil.com'],
23+
port: defaultPort,
24+
sslBump: true,
25+
caFiles: { certPath: '/tmp/cert.pem', keyPath: '/tmp/key.pem' },
26+
sslDbPath: '/tmp/ssl_db',
27+
urlPatterns: [maliciousPattern],
28+
});
29+
}).toThrow(/SECURITY/);
30+
});
31+
32+
it('should reject hash character in URL pattern (Squid comment injection)', () => {
33+
const maliciousPattern = 'https://evil.com/path#http_access allow all';
34+
expect(() => {
35+
generateSquidConfig({
36+
domains: ['evil.com'],
37+
port: defaultPort,
38+
sslBump: true,
39+
caFiles: { certPath: '/tmp/cert.pem', keyPath: '/tmp/key.pem' },
40+
sslDbPath: '/tmp/ssl_db',
41+
urlPatterns: [maliciousPattern],
42+
});
43+
}).toThrow(/SECURITY/);
44+
});
45+
46+
it('should reject semicolon in URL pattern (Squid token injection)', () => {
47+
const maliciousPattern = 'https://evil.com/path;injected';
48+
expect(() => {
49+
generateSquidConfig({
50+
domains: ['evil.com'],
51+
port: defaultPort,
52+
sslBump: true,
53+
caFiles: { certPath: '/tmp/cert.pem', keyPath: '/tmp/key.pem' },
54+
sslDbPath: '/tmp/ssl_db',
55+
urlPatterns: [maliciousPattern],
56+
});
57+
}).toThrow(/SECURITY/);
58+
});
59+
60+
it('should reject space in domain (ACL token injection)', () => {
61+
expect(() => {
62+
generateSquidConfig({
63+
domains: ['.evil.com .attacker.com'],
64+
port: defaultPort,
65+
});
66+
}).toThrow();
67+
});
68+
});
69+
470
// Pattern constant for the safer domain character class (matches the implementation)
571
const DOMAIN_CHAR_PATTERN = '[a-zA-Z0-9.-]*';
672

src/squid-config.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isDomainMatchedByPattern,
55
PlainDomainEntry,
66
DomainPattern,
7+
SQUID_DANGEROUS_CHARS,
78
} from './domain-patterns';
89
import { generateDlpSquidConfig } from './dlp';
910
import { DEFAULT_DNS_SERVERS } from './dns-resolver';
@@ -54,10 +55,26 @@ interface PatternsByProtocol {
5455
both: DomainPattern[];
5556
}
5657

58+
/**
59+
* Defense-in-depth: assert a domain/regex/URL-pattern string is safe for Squid config interpolation.
60+
* Rejects whitespace, null bytes, quotes, semicolons, backticks, hash characters, and backslashes —
61+
* all of which can inject directives, tokens, or comments into Squid config.
62+
*/
63+
function assertSafeForSquidConfig(value: string): string {
64+
if (SQUID_DANGEROUS_CHARS.test(value)) {
65+
throw new Error(
66+
`SECURITY: Domain or pattern contains characters unsafe for Squid config and cannot be ` +
67+
`interpolated into squid.conf: ${JSON.stringify(value)}`
68+
);
69+
}
70+
return value;
71+
}
72+
5773
/**
5874
* Helper to add leading dot to domain for Squid subdomain matching
5975
*/
6076
function formatDomainForSquid(domain: string): string {
77+
assertSafeForSquidConfig(domain);
6178
return domain.startsWith('.') ? domain : `.${domain}`;
6279
}
6380

@@ -152,7 +169,7 @@ function generateSslBumpSection(
152169
let urlAccessRules = '';
153170
if (urlPatterns && urlPatterns.length > 0) {
154171
const urlAcls = urlPatterns
155-
.map((pattern, i) => `acl allowed_url_${i} url_regex ${pattern}`)
172+
.map((pattern, i) => `acl allowed_url_${i} url_regex ${assertSafeForSquidConfig(pattern)}`)
156173
.join('\n');
157174
urlAclSection = `\n# URL pattern ACLs for HTTPS content inspection\n${urlAcls}\n`;
158175

0 commit comments

Comments
 (0)