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