forked from ChromeDevTools/chrome-devtools-mcp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathurlValidator.ts
More file actions
117 lines (100 loc) · 2.75 KB
/
urlValidator.ts
File metadata and controls
117 lines (100 loc) · 2.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {Debugger} from 'debug';
export class UrlValidator {
#allowedOrigins: string[];
#blockedOrigins: string[];
#logger: Debugger;
constructor(
options: {
allowedOrigins?: string[];
blockedOrigins?: string[];
},
logger: Debugger,
) {
this.#allowedOrigins = options.allowedOrigins ?? [];
this.#blockedOrigins = options.blockedOrigins ?? [];
this.#logger = logger;
if (this.#allowedOrigins.length > 0) {
this.#logger(
`URL validation enabled. Allowed origins: ${this.#allowedOrigins.join(', ')}`,
);
}
if (this.#blockedOrigins.length > 0) {
this.#logger(
`URL validation enabled. Blocked origins: ${this.#blockedOrigins.join(', ')}`,
);
}
}
static parseOrigins(originsString?: string): string[] {
if (!originsString) {
return [];
}
return originsString
.split(';')
.map(o => o.trim())
.filter(o => o.length > 0);
}
isAllowed(url: string): boolean {
if (this.#isSpecialUrl(url)) {
return true;
}
try {
const origin = new URL(url).origin;
if (this.#matchesAnyOrigin(origin, this.#blockedOrigins)) {
this.#logger(`Blocked request to ${url} (origin: ${origin})`);
return false;
}
if (this.#allowedOrigins.length === 0) {
return true;
}
const allowed = this.#matchesAnyOrigin(origin, this.#allowedOrigins);
if (!allowed) {
this.#logger(
`Blocked request to ${url} (origin: ${origin} not in allowlist)`,
);
}
return allowed;
} catch {
return true;
}
}
#isSpecialUrl(url: string): boolean {
const lowerUrl = url.toLowerCase();
return (
lowerUrl.startsWith('about:') ||
lowerUrl.startsWith('data:') ||
lowerUrl.startsWith('blob:') ||
lowerUrl.startsWith('file:')
);
}
#matchesAnyOrigin(origin: string, patterns: string[]): boolean {
for (const pattern of patterns) {
if (this.#matchesOriginPattern(origin, pattern)) {
return true;
}
}
return false;
}
#matchesOriginPattern(origin: string, pattern: string): boolean {
if (origin === pattern) {
return true;
}
if (pattern.includes('*')) {
const regex = this.#patternToRegex(pattern);
return regex.test(origin);
}
return false;
}
#patternToRegex(pattern: string): RegExp {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
const regexPattern = escaped.replace(/\*/g, '[^\\/]+');
return new RegExp(`^${regexPattern}$`);
}
hasRestrictions(): boolean {
return this.#allowedOrigins.length > 0 || this.#blockedOrigins.length > 0;
}
}