16
16
17
17
package com .netflix .spinnaker .orca .config ;
18
18
19
+ import com .google .common .net .InetAddresses ;
19
20
import java .net .InetAddress ;
20
21
import java .net .NetworkInterface ;
22
+ import java .net .SocketException ;
21
23
import java .net .URI ;
24
+ import java .net .UnknownHostException ;
22
25
import java .util .ArrayList ;
23
26
import java .util .Arrays ;
24
27
import java .util .Collection ;
25
28
import java .util .Collections ;
26
29
import java .util .HashSet ;
27
30
import java .util .List ;
31
+ import java .util .Objects ;
28
32
import java .util .Optional ;
29
33
import java .util .Set ;
30
34
import java .util .regex .Pattern ;
35
+ import java .util .stream .Collectors ;
31
36
import lombok .AllArgsConstructor ;
32
37
import lombok .Data ;
33
38
import lombok .NoArgsConstructor ;
37
42
public class UserConfiguredUrlRestrictions {
38
43
@ Data
39
44
public static class Builder {
40
- private String allowedHostnamesRegex = ".*" ;
45
+ private String allowedHostnamesRegex =
46
+ ".*\\ ..+" ; // Exclude anything without a dot, since k8s resolves single-word names
41
47
private List <String > allowedSchemes = new ArrayList <>(Arrays .asList ("http" , "https" ));
42
48
private boolean rejectLocalhost = true ;
43
49
private boolean rejectLinkLocal = true ;
50
+ private boolean rejectVerbatimIps = true ;
44
51
private HttpClientProperties httpClientProperties = new HttpClientProperties ();
45
52
private List <String > rejectedIps =
46
53
new ArrayList <>(); // can contain IP addresses and/or IP ranges (CIDR block)
47
54
55
+ // Blanket exclusion on certain domains
56
+ // This default pattern will exclude anything that is suffixed with the excluded domain
57
+ private String excludedDomainTemplate = "(?=.+\\ .%s$).*\\ ..+" ;
58
+ private List <String > excludedDomains = List .of ("spinnaker" , "local" , "localdomain" , "internal" );
59
+ // Generate exclusion patterns based on the values of environment variables
60
+ // Useful for dynamically excluding all requests to the current k8s namespace, for example
61
+ private List <String > excludedDomainsFromEnvironment = List .of ();
62
+ private List <String > extraExcludedPatterns = List .of ();
63
+
48
64
public Builder withAllowedHostnamesRegex (String allowedHostnamesRegex ) {
49
65
setAllowedHostnamesRegex (allowedHostnamesRegex );
50
66
return this ;
@@ -65,6 +81,11 @@ public Builder withRejectLinkLocal(boolean rejectLinkLocal) {
65
81
return this ;
66
82
}
67
83
84
+ public Builder withRejectVerbatimIps (boolean rejectVerbatimIps ) {
85
+ setRejectVerbatimIps (rejectVerbatimIps );
86
+ return this ;
87
+ }
88
+
68
89
public Builder withRejectedIps (List <String > rejectedIpRanges ) {
69
90
setRejectedIps (rejectedIpRanges );
70
91
return this ;
@@ -75,43 +96,122 @@ public Builder withHttpClientProperties(HttpClientProperties httpClientPropertie
75
96
return this ;
76
97
}
77
98
99
+ public Builder withExcludedDomainsFromEnvironment (List <String > envVars ) {
100
+ setExcludedDomainsFromEnvironment (envVars );
101
+ return this ;
102
+ }
103
+
104
+ public Builder withExtraExcludedPatterns (List <String > patterns ) {
105
+ setExtraExcludedPatterns (patterns );
106
+ return this ;
107
+ }
108
+
109
+ String getEnvValue (String envVarName ) {
110
+ return System .getenv (envVarName );
111
+ }
112
+
113
+ List <String > getEnvValues (List <String > envVars ) {
114
+ if (envVars == null ) return List .of ();
115
+
116
+ return envVars .stream ()
117
+ .map (this ::getEnvValue )
118
+ .filter (Objects ::nonNull )
119
+ .collect (Collectors .toList ());
120
+ }
121
+
122
+ List <Pattern > compilePatterns (List <String > values , String patternStr , boolean quote ) {
123
+ if (values == null || patternStr == null ) {
124
+ return List .of ();
125
+ }
126
+
127
+ return values .stream ()
128
+ .map (value -> quote ? Pattern .quote (value ) : value )
129
+ .map (value -> Pattern .compile (String .format (patternStr , value )))
130
+ .collect (Collectors .toList ());
131
+ }
132
+
78
133
public UserConfiguredUrlRestrictions build () {
134
+ // Combine and build all excluded domains based on the specified names, env vars, and pattern
135
+ List <String > allExcludedDomains = new ArrayList <>();
136
+ allExcludedDomains .addAll (excludedDomains );
137
+ allExcludedDomains .addAll (getEnvValues (excludedDomainsFromEnvironment ));
138
+
139
+ // Collect any extra patterns and provide the final list of patterns
140
+ List <Pattern > allExcludedPatterns = new ArrayList <>();
141
+ allExcludedPatterns .addAll (compilePatterns (allExcludedDomains , excludedDomainTemplate , true ));
142
+ allExcludedPatterns .addAll (compilePatterns (extraExcludedPatterns , "%s" , false ));
143
+
79
144
return new UserConfiguredUrlRestrictions (
80
145
Pattern .compile (allowedHostnamesRegex ),
81
146
allowedSchemes ,
82
147
rejectLocalhost ,
83
148
rejectLinkLocal ,
149
+ rejectVerbatimIps ,
84
150
rejectedIps ,
85
- httpClientProperties );
151
+ httpClientProperties ,
152
+ allExcludedPatterns );
86
153
}
87
154
}
88
155
89
156
private final Pattern allowedHostnames ;
90
157
private final Set <String > allowedSchemes ;
91
158
private final boolean rejectLocalhost ;
92
159
private final boolean rejectLinkLocal ;
160
+ private final boolean rejectVerbatimIps ;
93
161
private final Set <String > rejectedIps ;
94
162
private final HttpClientProperties clientProperties ;
163
+ private final List <Pattern > excludedPatterns ;
95
164
96
- public UserConfiguredUrlRestrictions (
165
+ protected UserConfiguredUrlRestrictions (
97
166
Pattern allowedHostnames ,
98
167
Collection <String > allowedSchemes ,
99
168
boolean rejectLocalhost ,
100
169
boolean rejectLinkLocal ,
170
+ boolean rejectVerbatimIps ,
101
171
Collection <String > rejectedIps ,
102
- HttpClientProperties clientProperties ) {
172
+ HttpClientProperties clientProperties ,
173
+ List <Pattern > excludedPatterns ) {
103
174
this .allowedHostnames = allowedHostnames ;
104
175
this .allowedSchemes =
105
176
allowedSchemes == null
106
177
? Collections .emptySet ()
107
178
: Collections .unmodifiableSet (new HashSet <>(allowedSchemes ));
108
179
this .rejectLocalhost = rejectLocalhost ;
109
180
this .rejectLinkLocal = rejectLinkLocal ;
181
+ this .rejectVerbatimIps = rejectVerbatimIps ;
110
182
this .rejectedIps =
111
183
rejectedIps == null
112
184
? Collections .emptySet ()
113
185
: Collections .unmodifiableSet (new HashSet <>(rejectedIps ));
114
186
this .clientProperties = clientProperties ;
187
+ this .excludedPatterns = excludedPatterns ;
188
+ }
189
+
190
+ InetAddress resolveHost (String host ) throws UnknownHostException {
191
+ return InetAddress .getByName (host );
192
+ }
193
+
194
+ boolean isLocalhost (InetAddress addr ) throws SocketException {
195
+ return addr .isLoopbackAddress ()
196
+ || Optional .ofNullable (NetworkInterface .getByInetAddress (addr )).isPresent ();
197
+ }
198
+
199
+ boolean isLinkLocal (InetAddress addr ) {
200
+ return addr .isLinkLocalAddress ();
201
+ }
202
+
203
+ boolean isValidHostname (String host ) {
204
+ return allowedHostnames .matcher (host ).matches ()
205
+ && excludedPatterns .stream ().noneMatch (p -> p .matcher (host ).matches ());
206
+ }
207
+
208
+ boolean isValidIpAddress (String host ) {
209
+ var matcher = new IpAddressMatcher (host );
210
+ return rejectedIps .stream ().noneMatch (matcher ::matches );
211
+ }
212
+
213
+ boolean isIpAddress (String host ) {
214
+ return InetAddresses .isInetAddress (host );
115
215
}
116
216
117
217
public URI validateURI (String url ) throws IllegalArgumentException {
@@ -130,12 +230,17 @@ public URI validateURI(String url) throws IllegalArgumentException {
130
230
if (host == null ) {
131
231
String authority = u .getAuthority ();
132
232
if (authority != null ) {
133
- int portIndex = authority .indexOf (":" );
134
- host = (portIndex > -1 ) ? authority .substring (0 , portIndex ) : authority ;
233
+ // Don't attempt to colon-substring ipv6 addresses
234
+ if (isIpAddress (authority )) {
235
+ host = authority ;
236
+ } else {
237
+ int portIndex = authority .indexOf (":" );
238
+ host = (portIndex > -1 ) ? authority .substring (0 , portIndex ) : authority ;
239
+ }
135
240
}
136
241
}
137
242
138
- if (host == null ) {
243
+ if (host == null || host . isEmpty () ) {
139
244
throw new IllegalArgumentException ("Unable to determine host for the url provided " + url );
140
245
}
141
246
@@ -144,37 +249,40 @@ public URI validateURI(String url) throws IllegalArgumentException {
144
249
"Allowed Hostnames are not set, external HTTP requests are not enabled. Please configure 'user-configured-url-restrictions.allowedHostnamesRegex' in your orca config." );
145
250
}
146
251
147
- if (!allowedHostnames .matcher (host ).matches ()) {
148
- throw new IllegalArgumentException (
149
- "Host not allowed " + host + ". Host much match " + allowedHostnames .toString () + "." );
150
- }
252
+ // Strip ipv6 brackets if present
253
+ // InetAddress.getHost() retains them, but other code doesn't quite understand
254
+ host = host .replace ("[" , "" ).replace ("]" , "" );
151
255
152
- if (rejectLocalhost || rejectLinkLocal ) {
153
- InetAddress addr = InetAddress .getByName (host );
154
- if (rejectLocalhost ) {
155
- if (addr .isLoopbackAddress ()
156
- || Optional .ofNullable (NetworkInterface .getByInetAddress (addr )).isPresent ()) {
157
- throw new IllegalArgumentException ("invalid address for " + host );
158
- }
159
- }
160
- if (rejectLinkLocal && addr .isLinkLocalAddress ()) {
161
- throw new IllegalArgumentException ("invalid address for " + host );
162
- }
256
+ if (isIpAddress (host ) && rejectVerbatimIps ) {
257
+ throw new IllegalArgumentException ("Verbatim IP addresses are not allowed" );
163
258
}
164
259
165
- for (String ip : rejectedIps ) {
166
- IpAddressMatcher ipMatcher = new IpAddressMatcher (ip );
260
+ var addr = resolveHost (host );
261
+ var isLocalhost = isLocalhost (addr );
262
+ var isLinkLocal = isLinkLocal (addr );
263
+
264
+ if ((isLocalhost && rejectLocalhost ) || (isLinkLocal && rejectLinkLocal )) {
265
+ throw new IllegalArgumentException ("Host not allowed: " + host );
266
+ }
167
267
168
- if (ipMatcher .matches (host )) {
169
- throw new IllegalArgumentException ("address " + host + " is within rejected IPs: " + ip );
268
+ if (!isValidHostname (host ) && !isIpAddress (host )) {
269
+ // If localhost or link local is allowed, that takes precedence over the name filter
270
+ // This avoids the need to include local names in the hostname pattern in addition to
271
+ // setting the local config flag
272
+ if (!(isLocalhost || isLinkLocal )) {
273
+ throw new IllegalArgumentException ("Host not allowed: " + host );
170
274
}
171
275
}
172
276
277
+ if (!isValidIpAddress (host )) {
278
+ throw new IllegalArgumentException ("Address not allowed: " + host );
279
+ }
280
+
173
281
return u ;
174
282
} catch (IllegalArgumentException iae ) {
175
283
throw iae ;
176
284
} catch (Exception ex ) {
177
- throw new IllegalArgumentException ("URI not valid " + url , ex );
285
+ throw new IllegalArgumentException ("URI not valid: " + url , ex );
178
286
}
179
287
}
180
288
0 commit comments