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,117 @@ public Builder withHttpClientProperties(HttpClientProperties httpClientPropertie
75
96
return this ;
76
97
}
77
98
99
+ public Builder withExtraExcludedPatterns (List <String > patterns ) {
100
+ setExtraExcludedPatterns (patterns );
101
+ return this ;
102
+ }
103
+
104
+ String getEnvValue (String envVarName ) {
105
+ return System .getenv (envVarName );
106
+ }
107
+
108
+ List <String > getEnvValues (List <String > envVars ) {
109
+ if (envVars == null ) return List .of ();
110
+
111
+ return envVars .stream ()
112
+ .map (this ::getEnvValue )
113
+ .filter (Objects ::nonNull )
114
+ .collect (Collectors .toList ());
115
+ }
116
+
117
+ List <Pattern > compilePatterns (List <String > values , String patternStr , boolean quote ) {
118
+ if (values == null || patternStr == null ) {
119
+ return List .of ();
120
+ }
121
+
122
+ return values .stream ()
123
+ .map (value -> quote ? Pattern .quote (value ) : value )
124
+ .map (value -> Pattern .compile (String .format (patternStr , value )))
125
+ .collect (Collectors .toList ());
126
+ }
127
+
78
128
public UserConfiguredUrlRestrictions build () {
129
+ // Combine and build all excluded domains based on the specified names, env vars, and pattern
130
+ List <String > allExcludedDomains = new ArrayList <>();
131
+ allExcludedDomains .addAll (excludedDomains );
132
+ allExcludedDomains .addAll (getEnvValues (excludedDomainsFromEnvironment ));
133
+
134
+ // Collect any extra patterns and provide the final list of patterns
135
+ List <Pattern > allExcludedPatterns = new ArrayList <>();
136
+ allExcludedPatterns .addAll (compilePatterns (allExcludedDomains , excludedDomainTemplate , true ));
137
+ allExcludedPatterns .addAll (compilePatterns (extraExcludedPatterns , "%s" , false ));
138
+
79
139
return new UserConfiguredUrlRestrictions (
80
140
Pattern .compile (allowedHostnamesRegex ),
81
141
allowedSchemes ,
82
142
rejectLocalhost ,
83
143
rejectLinkLocal ,
144
+ rejectVerbatimIps ,
84
145
rejectedIps ,
85
- httpClientProperties );
146
+ httpClientProperties ,
147
+ allExcludedPatterns );
86
148
}
87
149
}
88
150
89
151
private final Pattern allowedHostnames ;
90
152
private final Set <String > allowedSchemes ;
91
153
private final boolean rejectLocalhost ;
92
154
private final boolean rejectLinkLocal ;
155
+ private final boolean rejectVerbatimIps ;
93
156
private final Set <String > rejectedIps ;
94
157
private final HttpClientProperties clientProperties ;
158
+ private final List <Pattern > excludedPatterns ;
95
159
96
- public UserConfiguredUrlRestrictions (
160
+ protected UserConfiguredUrlRestrictions (
97
161
Pattern allowedHostnames ,
98
162
Collection <String > allowedSchemes ,
99
163
boolean rejectLocalhost ,
100
164
boolean rejectLinkLocal ,
165
+ boolean rejectVerbatimIps ,
101
166
Collection <String > rejectedIps ,
102
- HttpClientProperties clientProperties ) {
167
+ HttpClientProperties clientProperties ,
168
+ List <Pattern > excludedPatterns ) {
103
169
this .allowedHostnames = allowedHostnames ;
104
170
this .allowedSchemes =
105
171
allowedSchemes == null
106
172
? Collections .emptySet ()
107
173
: Collections .unmodifiableSet (new HashSet <>(allowedSchemes ));
108
174
this .rejectLocalhost = rejectLocalhost ;
109
175
this .rejectLinkLocal = rejectLinkLocal ;
176
+ this .rejectVerbatimIps = rejectVerbatimIps ;
110
177
this .rejectedIps =
111
178
rejectedIps == null
112
179
? Collections .emptySet ()
113
180
: Collections .unmodifiableSet (new HashSet <>(rejectedIps ));
114
181
this .clientProperties = clientProperties ;
182
+ this .excludedPatterns = excludedPatterns ;
183
+ }
184
+
185
+ InetAddress resolveHost (String host ) throws UnknownHostException {
186
+ return InetAddress .getByName (host );
187
+ }
188
+
189
+ boolean isLocalhost (InetAddress addr ) throws SocketException {
190
+ return addr .isLoopbackAddress ()
191
+ || Optional .ofNullable (NetworkInterface .getByInetAddress (addr )).isPresent ();
192
+ }
193
+
194
+ boolean isLinkLocal (InetAddress addr ) {
195
+ return addr .isLinkLocalAddress ();
196
+ }
197
+
198
+ boolean isValidHostname (String host ) {
199
+ return allowedHostnames .matcher (host ).matches ()
200
+ && excludedPatterns .stream ().noneMatch (p -> p .matcher (host ).matches ());
201
+ }
202
+
203
+ boolean isValidIpAddress (String host ) {
204
+ var matcher = new IpAddressMatcher (host );
205
+ return rejectedIps .stream ().noneMatch (matcher ::matches );
206
+ }
207
+
208
+ boolean isIpAddress (String host ) {
209
+ return InetAddresses .isInetAddress (host );
115
210
}
116
211
117
212
public URI validateURI (String url ) throws IllegalArgumentException {
@@ -130,12 +225,17 @@ public URI validateURI(String url) throws IllegalArgumentException {
130
225
if (host == null ) {
131
226
String authority = u .getAuthority ();
132
227
if (authority != null ) {
133
- int portIndex = authority .indexOf (":" );
134
- host = (portIndex > -1 ) ? authority .substring (0 , portIndex ) : authority ;
228
+ // Don't attempt to colon-substring ipv6 addresses
229
+ if (isIpAddress (authority )) {
230
+ host = authority ;
231
+ } else {
232
+ int portIndex = authority .indexOf (":" );
233
+ host = (portIndex > -1 ) ? authority .substring (0 , portIndex ) : authority ;
234
+ }
135
235
}
136
236
}
137
237
138
- if (host == null ) {
238
+ if (host == null || host . isEmpty () ) {
139
239
throw new IllegalArgumentException ("Unable to determine host for the url provided " + url );
140
240
}
141
241
@@ -144,37 +244,40 @@ public URI validateURI(String url) throws IllegalArgumentException {
144
244
"Allowed Hostnames are not set, external HTTP requests are not enabled. Please configure 'user-configured-url-restrictions.allowedHostnamesRegex' in your orca config." );
145
245
}
146
246
147
- if (!allowedHostnames .matcher (host ).matches ()) {
148
- throw new IllegalArgumentException (
149
- "Host not allowed " + host + ". Host much match " + allowedHostnames .toString () + "." );
150
- }
247
+ // Strip ipv6 brackets if present
248
+ // InetAddress.getHost() retains them, but other code doesn't quite understand
249
+ host = host .replace ("[" , "" ).replace ("]" , "" );
151
250
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
- }
251
+ if (isIpAddress (host ) && rejectVerbatimIps ) {
252
+ throw new IllegalArgumentException ("Verbatim IP addresses are not allowed" );
163
253
}
164
254
165
- for (String ip : rejectedIps ) {
166
- IpAddressMatcher ipMatcher = new IpAddressMatcher (ip );
255
+ var addr = resolveHost (host );
256
+ var isLocalhost = isLocalhost (addr );
257
+ var isLinkLocal = isLinkLocal (addr );
258
+
259
+ if ((isLocalhost && rejectLocalhost ) || (isLinkLocal && rejectLinkLocal )) {
260
+ throw new IllegalArgumentException ("Host not allowed: " + host );
261
+ }
167
262
168
- if (ipMatcher .matches (host )) {
169
- throw new IllegalArgumentException ("address " + host + " is within rejected IPs: " + ip );
263
+ if (!isValidHostname (host ) && !isIpAddress (host )) {
264
+ // If localhost or link local is allowed, that takes precedence over the name filter
265
+ // This avoids the need to include local names in the hostname pattern in addition to
266
+ // setting the local config flag
267
+ if (!(isLocalhost || isLinkLocal )) {
268
+ throw new IllegalArgumentException ("Host not allowed: " + host );
170
269
}
171
270
}
172
271
272
+ if (!isValidIpAddress (host )) {
273
+ throw new IllegalArgumentException ("Address not allowed: " + host );
274
+ }
275
+
173
276
return u ;
174
277
} catch (IllegalArgumentException iae ) {
175
278
throw iae ;
176
279
} catch (Exception ex ) {
177
- throw new IllegalArgumentException ("URI not valid " + url , ex );
280
+ throw new IllegalArgumentException ("URI not valid: " + url , ex );
178
281
}
179
282
}
180
283
0 commit comments