001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.http.remote; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.net.*; 022import java.util.*; 023import java.util.concurrent.atomic.*; 024import java.util.regex.*; 025import java.util.stream.*; 026 027/** 028 * Shared SSRF-hardening policy for the {@link Remote @Remote}/{@link RemoteOp @RemoteOp} proxy engines. 029 * 030 * <p> 031 * Replaces the earlier scheme-only guard (<c>requireHttpScheme</c>) with a <b>deny-private</b> default policy: 032 * an absolute <c>http</c>/<c>https</c> {@code @Remote} URL (an explicit {@code @Url} value, a {@code baseUrl()} 033 * override, or an already-absolute default-path {@code fullPath}) is additionally checked against a deny-list of 034 * loopback, RFC1918/link-local, and cloud-metadata targets — both lexically (the literal host string) and, 035 * separately, against the <b>resolved</b> address actually connected to ("pin-on-connect", see 036 * {@link #selectAllowedAddress(String, boolean, AddressResolver)}). 037 * 038 * <p> 039 * Scheme-less (relative) values are unaffected — they resolve against the operator-configured client root, 040 * which is not re-checked by this policy. 041 * 042 * <p> 043 * Both engines (next-generation {@code RestClient.remote(...)} and classic {@code RestClient.getRemote(...)}) call 044 * into this single shared class so the deny-list and pin-selection logic are defined exactly once. 045 * 046 * @since 10.0.0 047 */ 048@SuppressWarnings({ 049 "java:S125", // Comments document URI-template blanking; they are not commented-out code. 050 "java:S3516" // requireAllowedUrl is a validation passthrough: success returns the same URL that was supplied. 051}) 052public final class RemoteUrlPolicy { 053 054 private RemoteUrlPolicy() {} 055 056 /** The maximum number of redirect hops a policy-covered {@code @Remote} call will follow before failing. */ 057 public static final int MAX_REDIRECT_HOPS = 5; 058 059 /** The {@code RestClient} builder/settings key for the {@code allowPrivateUrls} opt-in (system property fallback). */ 060 public static final String ALLOW_PRIVATE_URLS_PROPERTY = "RestClient.allowPrivateUrls"; 061 062 /** 063 * The system property holding the narrow lab allow-list: a comma- or whitespace-separated list of CIDR ranges 064 * (e.g. {@code 198.18.0.0/15,192.0.0.8}) that re-permit addresses denied only because they fall in one of the 065 * special-purpose ranges <c>192.0.0.0/24</c>, <c>198.18.0.0/15</c>, <c>224.0.0.0/4</c>, <c>240.0.0.0/4</c> or 066 * <c>ff00::/8</c>. Unset or blank (the default) allows nothing. 067 * 068 * <p> 069 * The allow is narrow by construction: every entry must lie entirely inside one of those five ranges (anything 070 * else, including <c>0.0.0.0/0</c> and <c>::/0</c>, is rejected with an {@link IllegalArgumentException}), and it 071 * never re-permits loopback, link-local/metadata, private, CGNAT, this-network, Teredo, local-use NAT64 or any 072 * other denied address, unlike the all-or-nothing {@link #ALLOW_PRIVATE_URLS_PROPERTY} opt-in. An IPv4 entry 073 * matches an IPv4 address or the IPv4 address embedded in an IPv6 form; an IPv6 entry matches only a native 074 * <c>ff00::/8</c> address. 075 */ 076 public static final String ALLOW_ADDRESS_RANGES_PROPERTY = "RestClient.allowAddressRanges"; 077 078 // IPv4 literal: 4 dot-separated groups of 1-3 digits (over-permissive on range; InetAddress.getByName validates). 079 private static final Pattern IPV4_LITERAL = Pattern.compile("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$"); 080 081 // Host names (case-insensitive) that are always denied by default, independent of DNS resolution. 082 private static final Set<String> DENIED_HOST_NAMES = Set.of("localhost", "localhost.localdomain"); 083 084 private static final String METADATA_HOST = "metadata.google.internal"; 085 private static final String METADATA_HOST_SUFFIX = "." + METADATA_HOST; 086 087 /** 088 * Resolves the {@code InetAddress} candidates for a hostname. Injectable so pin-on-connect tests can simulate 089 * DNS rebinding / mixed A+AAAA / blocked-address resolution without depending on live (and flaky) DNS. 090 */ 091 @FunctionalInterface 092 public interface AddressResolver { 093 094 /** 095 * Resolves {@code host} to its candidate addresses. 096 * 097 * @param host The hostname (or IP literal) to resolve. 098 * @return The resolved candidate addresses. Never <jk>null</jk> or empty on success. 099 * @throws UnknownHostException If the host cannot be resolved. 100 */ 101 InetAddress[] resolve(String host) throws UnknownHostException; 102 103 /** The production resolver: real DNS resolution via {@link InetAddress#getAllByName(String)}. */ 104 AddressResolver DEFAULT = InetAddress::getAllByName; 105 } 106 107 /** 108 * Enforces the SSRF guardrail on an effective {@code @Remote} URL: when {@code url} carries a URI scheme, it 109 * must be a canonical absolute {@code http}/{@code https} URL with a nonempty host, and — unless 110 * {@code allowPrivateUrls} is set — the literal host must not match the deny-list (see class javadoc). 111 * Scheme-less (relative) values pass through unchanged (they resolve against the client root, which is not 112 * re-checked here). 113 * 114 * <p> 115 * This is the <b>pre-check</b> stage only. It does not resolve DNS; a hostname that is not itself denied but 116 * resolves to a denied address is instead caught by {@link #selectAllowedAddress(String, boolean, AddressResolver)} 117 * ("pin-on-connect") at connect time, including on every followed redirect hop. 118 * 119 * @param url The effective URL/path to validate. 120 * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped (the http/https scheme requirement 121 * and canonical-URI requirement still apply). 122 * @return The unchanged {@code url}. 123 * @throws IllegalArgumentException If {@code url} is absolute and violates the policy. 124 */ 125 public static String requireAllowedUrl(String url, boolean allowPrivateUrls) { 126 return requireAllowedUrl(url, allowPrivateUrls, AllowedRanges.NONE); 127 } 128 129 /** 130 * Same as {@link #requireAllowedUrl(String, boolean)}, additionally re-permitting literal hosts that fall inside 131 * the per-client {@code allowedRanges} (unioned with the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property). 132 * 133 * @param url The effective URL/path to validate. 134 * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped. 135 * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>. 136 * @return The unchanged {@code url}. 137 * @throws IllegalArgumentException If {@code url} is absolute and violates the policy. 138 */ 139 public static String requireAllowedUrl(String url, boolean allowPrivateUrls, AllowedRanges allowedRanges) { 140 var scheme = schemeOf(url); 141 if (scheme == null) 142 return url; // Scheme-less relative value; resolved against the client root, not re-checked here. 143 144 if (! (eqic(scheme, "http") || eqic(scheme, "https"))) 145 throw iaex("Unsupported URL scheme '%s' in @Remote URL override; only http/https are allowed: %s", scheme, url); 146 147 URI uri; 148 try { 149 // SVL/@Path {var} tokens (e.g. "http://host/u/{id}") are resolved later, at request-build time -- {} 150 // is not valid URI syntax, so blank out any such token before parsing (the sanitized copy is used only 151 // to classify scheme/host here; the original url, template tokens intact, is what is returned/used). 152 uri = new URI(blankOutTemplateTokens(url)); 153 } catch (URISyntaxException e) { 154 throw iaex(e, "Invalid @Remote URL override: %s", url); 155 } 156 var host = uri.getHost(); 157 if (ie(host)) 158 throw iaex("@Remote URL override has no host (non-canonical or opaque form is not allowed): %s", url); 159 160 if (! allowPrivateUrls && isDeniedHost(host, allowedRanges)) 161 throw iaex("@Remote URL override targets a private/loopback/link-local/metadata/reserved host (including CGNAT, this-network, special-purpose/multicast/reserved, Teredo and local-use NAT64 ranges), which is denied by default (set allowPrivateUrls to allow local-dev targets, or list a special-purpose range such as 198.18.0.0/15 in the RestClient.allowAddressRanges system property or the client's allowAddressRanges setting): %s", url); 162 163 return url; 164 } 165 166 // Matches {name} SVL/@Path template tokens (no nested braces) so requireAllowedUrl can parse an unsubstituted 167 // @Url/@RemoteOp(path) value (tokens are filled in later, at request-build time) as a well-formed java.net.URI. 168 private static final Pattern TEMPLATE_TOKEN = Pattern.compile("\\{[^{}]*\\}"); 169 170 private static String blankOutTemplateTokens(String url) { 171 return url.indexOf('{') == -1 ? url : TEMPLATE_TOKEN.matcher(url).replaceAll("x"); 172 } 173 174 /** 175 * Returns <jk>true</jk> if the literal {@code host} string is denied by the default policy: a dotted-decimal 176 * IPv4/bracketed-IPv6 literal classified as denied by {@link #isDeniedAddress(InetAddress)} (an IPv6 zone id is 177 * ignored, and an unparseable literal containing a {@code :} is denied), 178 * or a case-insensitive match on {@code localhost}/{@code localhost.localdomain}/{@code metadata.google.internal} 179 * (and its subdomains). 180 * 181 * <p> 182 * A bare decimal/octal/hex IP literal (e.g. {@code http://2130706433}) does <b>not</b> look like a dotted IPv4 183 * literal and is intentionally left to {@link #selectAllowedAddress(String, boolean, AddressResolver)} (resolved 184 * at pin-on-connect time), not this lexical pre-check. 185 * 186 * @param host The literal host string (from {@link URI#getHost()}). 187 * @return <jk>true</jk> if the host is denied by the default policy. 188 */ 189 public static boolean isDeniedHost(String host) { 190 return isDeniedHost(host, AllowedRanges.NONE); 191 } 192 193 /** 194 * Same as {@link #isDeniedHost(String)}, taking the per-client {@code allowedRanges} into account. 195 * 196 * @param host The literal host string (from {@link URI#getHost()}). 197 * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>. 198 * @return <jk>true</jk> if the host is denied. 199 */ 200 public static boolean isDeniedHost(String host, AllowedRanges allowedRanges) { 201 var h = host.toLowerCase(Locale.ROOT); 202 if (DENIED_HOST_NAMES.contains(h) || h.equals(METADATA_HOST) || h.endsWith(METADATA_HOST_SUFFIX)) 203 return true; 204 if (looksLikeIpLiteral(host)) { 205 var literal = stripZoneId(host); 206 try { 207 return isDeniedAddress(InetAddress.getByName(literal), allowedRanges); 208 } catch (UnknownHostException e) { 209 // A colon-containing literal that cannot be parsed fails closed; a dotted-decimal form that is not valid 210 // (e.g. an octet > 255) is treated as an ordinary (non-denied) name and left to pin-on-connect. 211 return literal.indexOf(':') != -1; 212 } 213 } 214 return false; 215 } 216 217 // Removes an IPv6 zone/scope id (the '%...' suffix, or '%25...' as percent-encoded in a URL) from a possibly 218 // bracketed literal: "[fe80::1%25en0]" -> "[fe80::1]". The zone does not affect the address classification. 219 private static String stripZoneId(String host) { 220 var i = host.indexOf('%'); 221 if (i == -1) 222 return host; 223 return host.endsWith("]") ? host.substring(0, i) + "]" : host.substring(0, i); 224 } 225 226 /** 227 * Returns <jk>true</jk> if {@code host} is textually shaped like an IP literal (a dotted-decimal IPv4 form, or 228 * any form containing a {@code :} as in bracket-stripped IPv6) -- i.e. a form worth parsing locally (no DNS 229 * lookup) to classify without deferring to pin-on-connect. 230 */ 231 private static boolean looksLikeIpLiteral(String host) { 232 return IPV4_LITERAL.matcher(host).matches() || host.indexOf(':') != -1; 233 } 234 235 /** 236 * Returns <jk>true</jk> if {@code addr} is denied by the default policy -- the deny-list applied both to 237 * literal-IP hosts (pre-check) and to resolved addresses (pin-on-connect). 238 * 239 * <p> 240 * Denied addresses: 241 * <ul> 242 * <li>Loopback, unspecified, link-local (including the <c>169.254.169.254</c> metadata address) and RFC1918 243 * site-local addresses. 244 * <li>IPv4 <c>0.0.0.0/8</c> ("this network") and <c>100.64.0.0/10</c> (carrier-grade NAT shared space). 245 * <li>IPv4 special-purpose ranges that are never legitimate remote targets: <c>192.0.0.0/24</c> (IETF protocol 246 * assignments), <c>198.18.0.0/15</c> (benchmarking; often lab or appliance networks — see 247 * {@link #ALLOW_ADDRESS_RANGES_PROPERTY}), <c>224.0.0.0/4</c> (multicast) and <c>240.0.0.0/4</c> (reserved, 248 * including the limited broadcast address <c>255.255.255.255</c>). 249 * <li>IPv6 unique-local <c>fc00::/7</c> and multicast <c>ff00::/8</c>. 250 * <li>IPv6 Teredo <c>2001::/32</c>, denied outright because the embedded client IPv4 address is obfuscated. 251 * <li>IPv6 local-use NAT64 <c>64:ff9b:1::/48</c> (RFC 8215), denied outright because the embedding length 252 * (/48, /56, /64 or /96) is operator-defined and cannot be determined from the address. 253 * </ul> 254 * 255 * <p> 256 * IPv6 forms that embed an IPv4 address are unwrapped and classified by the embedded IPv4 address: 257 * IPv4-mapped (<c>::ffff:a.b.c.d</c>), IPv4-compatible (<c>::a.b.c.d</c>), IPv4-translated 258 * (<c>::ffff:0:a.b.c.d</c>), NAT64 (<c>64:ff9b::/96</c>) and 6to4 (<c>2002::/16</c>, IPv4 in bytes 2-5). 259 * A public embedded address (e.g. <c>64:ff9b::808:808</c>) remains allowed. 260 * 261 * <p> 262 * Addresses inside a range listed in the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property (empty by 263 * default) are re-permitted if they are denied only as a special-purpose range; see 264 * {@link #ALLOW_ADDRESS_RANGES_PROPERTY}. 265 * 266 * @param addr The address to classify. Must not be <jk>null</jk>. 267 * @return <jk>true</jk> if the address is denied, taking the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} allow-list 268 * into account. 269 * @throws IllegalArgumentException If the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} property is malformed. 270 */ 271 public static boolean isDeniedAddress(InetAddress addr) { 272 return isDeniedAddress(addr, AllowedRanges.NONE); 273 } 274 275 /** 276 * Same as {@link #isDeniedAddress(InetAddress)}, additionally re-permitting special-purpose addresses inside the 277 * per-client {@code allowedRanges}. The effective allow-list is the <b>union</b> of those ranges and the 278 * {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property; both are bounded to the same five special-purpose 279 * ranges, so the union can never re-open any other deny. 280 * 281 * @param addr The address to classify. Must not be <jk>null</jk>. 282 * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>. 283 * @return <jk>true</jk> if the address is denied. 284 * @throws IllegalArgumentException If the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} property is malformed. 285 */ 286 public static boolean isDeniedAddress(InetAddress addr, AllowedRanges allowedRanges) { 287 var ranges = parseAllowedRanges(System.getProperty(ALLOW_ADDRESS_RANGES_PROPERTY)); 288 if (! allowedRanges.isEmpty()) { 289 var union = new ArrayList<>(ranges); 290 union.addAll(allowedRanges.ranges); 291 ranges = union; 292 } 293 return isDenied(addr, ranges); 294 } 295 296 // Same as isDeniedAddress(InetAddress) but with an explicit allow-list spec instead of the system property. 297 static boolean isDeniedAddress(InetAddress addr, String allowedRanges) { 298 return isDenied(addr, parseAllowedRanges(allowedRanges)); 299 } 300 301 private static boolean isDenied(InetAddress addr, List<Cidr> ranges) { 302 if (isDeniedOther(addr)) 303 return true; // Never re-permitted: only special-purpose-range denials can be allowed. 304 if (! isSpecialPurpose(unwrapEmbeddedIpv4(addr))) 305 return false; 306 if (ranges.isEmpty()) 307 return true; 308 var raw = addr.getAddress(); 309 var embedded = unwrapEmbeddedIpv4(addr).getAddress(); 310 for (var r : ranges) 311 if (r.contains(raw) || r.contains(embedded)) 312 return false; 313 return true; 314 } 315 316 /** A parsed CIDR range. */ 317 private record Cidr(byte[] prefix, int bits) { 318 @Override /* Object */ 319 public boolean equals(Object o) { 320 return o instanceof Cidr other && bits == other.bits && Arrays.equals(prefix, other.prefix); 321 } 322 323 @Override /* Object */ 324 public int hashCode() { 325 return 31 * Arrays.hashCode(prefix) + bits; 326 } 327 328 @Override /* Object */ 329 public String toString() { 330 return "Cidr[prefix=" + Arrays.toString(prefix) + ", bits=" + bits + "]"; 331 } 332 333 boolean contains(byte[] b) { 334 if (b.length != prefix.length) 335 return false; 336 for (var i = 0; i < bits; i++) 337 if (((b[i >> 3] ^ prefix[i >> 3]) & (0x80 >> (i & 7))) != 0) 338 return false; 339 return true; 340 } 341 } 342 343 /** 344 * A validated, parsed per-client allow-list of special-purpose address ranges, the per-client counterpart of the 345 * {@value RemoteUrlPolicy#ALLOW_ADDRESS_RANGES_PROPERTY} system property. 346 * 347 * <p> 348 * It has exactly the property's narrow semantics: every entry must lie entirely inside <c>192.0.0.0/24</c>, 349 * <c>198.18.0.0/15</c>, <c>224.0.0.0/4</c>, <c>240.0.0.0/4</c> or <c>ff00::/8</c> (anything else is rejected with an 350 * {@link IllegalArgumentException}), entries are parsed as literals without any DNS lookup, and the ranges never 351 * re-permit loopback, link-local/metadata, private, CGNAT or any other denied address. Instances are immutable; 352 * two instances holding the same set of ranges are equal. 353 * 354 * @since 10.0.0 355 */ 356 public static final class AllowedRanges { 357 358 /** The empty allow-list, which re-permits nothing. */ 359 public static final AllowedRanges NONE = new AllowedRanges(List.of()); 360 361 private final List<Cidr> ranges; 362 private final Set<String> key; 363 364 private AllowedRanges(List<Cidr> ranges) { 365 this.ranges = ranges; 366 this.key = ranges.stream().map(r -> Arrays.toString(r.prefix()) + "/" + r.bits()).collect(Collectors.toCollection(TreeSet::new)); 367 } 368 369 /** 370 * Parses and validates allow-list entries. 371 * 372 * @param specs The entries; each element may itself hold several comma- or whitespace-separated CIDR ranges or 373 * single addresses. <jk>null</jk> or empty yields {@link #NONE}. 374 * @return The parsed allow-list. Never <jk>null</jk>. 375 * @throws IllegalArgumentException If any entry is malformed or not entirely inside one of the five 376 * special-purpose ranges. 377 */ 378 public static AllowedRanges of(String...specs) { 379 if (specs == null || specs.length == 0) 380 return NONE; 381 var list = new ArrayList<Cidr>(); 382 for (var spec : specs) { 383 if (spec == null) 384 throw iaex("Invalid allowed address range: null entry"); 385 list.addAll(parseAllowedRangesStrict(spec)); 386 } 387 return list.isEmpty() ? NONE : new AllowedRanges(List.copyOf(list)); 388 } 389 390 /** 391 * Returns <jk>true</jk> if this allow-list re-permits nothing. 392 * 393 * @return <jk>true</jk> if empty. 394 */ 395 public boolean isEmpty() { 396 return ranges.isEmpty(); 397 } 398 399 @Override /* Object */ 400 public boolean equals(Object o) { 401 return o instanceof AllowedRanges other && key.equals(other.key); 402 } 403 404 @Override /* Object */ 405 public int hashCode() { 406 return key.hashCode(); 407 } 408 409 @Override /* Object */ 410 public String toString() { 411 return "AllowedRanges" + key; 412 } 413 } 414 415 // Parses a spec without touching the single-entry property cache; an invalid entry throws. 416 private static List<Cidr> parseAllowedRangesStrict(String spec) { 417 return Arrays.stream(spec.trim().split("[,\\s]+")).filter(token -> ! token.isEmpty()).map(RemoteUrlPolicy::parseAllowEntry).toList(); 418 } 419 420 // The only ranges the allow-list may re-permit (and that every allow entry must lie within). 421 private static final List<Cidr> SPECIAL_PURPOSE = List.of( 422 new Cidr(new byte[]{(byte)192, 0, 0, 0}, 24), 423 new Cidr(new byte[]{(byte)198, 18, 0, 0}, 15), 424 new Cidr(new byte[]{(byte)224, 0, 0, 0}, 4), 425 new Cidr(new byte[]{(byte)240, 0, 0, 0}, 4), 426 new Cidr(new byte[]{(byte)0xFF, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 8) 427 ); 428 429 private static final Pattern ALLOW_ENTRY = Pattern.compile("^([\\d.]+|[\\dA-Fa-f.]*:[\\dA-Fa-f:.]*)(?:/(0|[1-9]\\d{0,2}))?$"); 430 431 // Single-entry cache (successes and failures): the spec is normally a constant system property, so parse once per 432 // distinct value. A failed parse is remembered by message so the same string throws again without re-parsing. 433 private record ParsedAllow(String spec, List<Cidr> ranges, String error) {} 434 private static final AtomicReference<ParsedAllow> PARSED_ALLOW = new AtomicReference<>(new ParsedAllow("", List.of(), null)); 435 436 private static List<Cidr> parseAllowedRanges(String spec) { 437 if (ib(spec)) 438 return List.of(); 439 var cached = PARSED_ALLOW.get(); 440 if (! cached.spec().equals(spec)) { 441 cached = parseAllowSpec(spec); 442 PARSED_ALLOW.set(cached); 443 } 444 if (cached.error() != null) 445 throw iaex("%s", cached.error()); 446 return cached.ranges(); 447 } 448 449 private static ParsedAllow parseAllowSpec(String spec) { 450 try { 451 return new ParsedAllow(spec, List.copyOf(parseAllowedRangesStrict(spec)), null); 452 } catch (IllegalArgumentException e) { 453 return new ParsedAllow(spec, List.of(), e.getMessage()); 454 } 455 } 456 457 // Parses a dotted-quad IPv4 literal purely syntactically (octets 0-255, no leading zeros); returns null if invalid. 458 @SuppressWarnings({ 459 "java:S1168" // null signals an invalid IPv4 literal (see comment); callers test for it 460 }) 461 private static byte[] parseIpv4(String s) { 462 var parts = s.split("\\.", -1); 463 if (parts.length != 4) 464 return null; 465 var out = new byte[4]; 466 for (var i = 0; i < 4; i++) { 467 var p = parts[i]; 468 if (! (p.equals("0") || p.matches("[1-9]\\d{0,2}"))) 469 return null; 470 var v = Integer.parseInt(p); 471 if (v > 255) 472 return null; 473 out[i] = (byte) v; 474 } 475 return out; 476 } 477 478 private static Cidr parseAllowEntry(String token) { 479 var m = ALLOW_ENTRY.matcher(token); 480 if (! m.matches()) 481 throw iaex("Invalid allowed address range '%s': expected an IPv4 or IPv6 literal with an optional /prefix", token); 482 var literal = m.group(1); 483 var isIpv6 = literal.indexOf(':') != -1; 484 InetAddress a; 485 try { 486 if (isIpv6) { 487 a = InetAddress.getByName(literal); // Contains ':' and only hex/colon/dot characters, so it is parsed as a literal (never resolved via DNS). 488 } else { 489 var v4 = parseIpv4(literal); 490 if (v4 == null) 491 throw iaex("Invalid allowed address range '%s': IPv4 addresses must be dotted-quad with octets 0-255 and no leading zeros", token); 492 a = InetAddress.getByAddress(v4); 493 } 494 } catch (UnknownHostException e) { 495 throw iaex(e, "Invalid allowed address range '%s': unparseable address", token); 496 } 497 // An IPv6-syntax entry must be a native IPv6 address (not collapsed to IPv4, e.g. ::ffff:a.b.c.d). 498 if (isIpv6 != (a instanceof Inet6Address)) 499 throw iaex("Invalid allowed address range '%s': IPv4-embedded IPv6 forms are not allowed here", token); 500 var prefix = a.getAddress(); 501 var bits = prefix.length * 8; 502 if (m.group(2) != null) { 503 bits = Integer.parseInt(m.group(2)); 504 if (bits > prefix.length * 8) 505 throw iaex("Invalid allowed address range '%s': prefix length must be 0-%s", token, prefix.length * 8); 506 } 507 for (var r : SPECIAL_PURPOSE) 508 if (r.prefix().length == prefix.length && bits >= r.bits() && r.contains(prefix)) 509 return new Cidr(prefix, bits); 510 throw iaex("Invalid allowed address range '%s': must lie entirely within 192.0.0.0/24, 198.18.0.0/15, 224.0.0.0/4, 240.0.0.0/4 or ff00::/8", token); 511 } 512 513 // True if the (already unwrapped) address is in one of the special-purpose ranges. 514 private static boolean isSpecialPurpose(InetAddress a) { 515 var bytes = a.getAddress(); 516 if (a instanceof Inet6Address) 517 return (bytes[0] & 0xFF) == 0xFF; 518 var b0 = bytes[0] & 0xFF; 519 var b1 = bytes[1] & 0xFF; 520 return (b0 == 192 && b1 == 0 && bytes[2] == 0) 521 || (b0 == 198 && (b1 & 0xFE) == 18) 522 || (b0 & 0xE0) == 0xE0; // 224.0.0.0/4 multicast + 240.0.0.0/4 reserved 523 } 524 525 // Every deny other than the special-purpose ranges (never subject to the allow-list). 526 private static boolean isDeniedOther(InetAddress addr) { 527 if (isTeredo(addr) || isNat64LocalUse(addr)) 528 return true; 529 var a = unwrapEmbeddedIpv4(addr); 530 if (a.isLoopbackAddress() || a.isAnyLocalAddress() || a.isLinkLocalAddress() || a.isSiteLocalAddress()) 531 return true; 532 var bytes = a.getAddress(); 533 if (a instanceof Inet6Address) 534 return (bytes[0] & 0xFE) == 0xFC; // Unique-local fc00::/7 (top 7 bits are 1111 110). 535 // 0.0.0.0/8 ("this network") and 100.64.0.0/10 (CGNAT shared space). 536 return bytes[0] == 0 || ((bytes[0] & 0xFF) == 100 && (bytes[1] & 0xC0) == 0x40); 537 } 538 539 /** Returns <jk>true</jk> for a Teredo (<c>2001:0000::/32</c>) address. */ 540 private static boolean isTeredo(InetAddress addr) { 541 if (! (addr instanceof Inet6Address)) 542 return false; 543 var b = addr.getAddress(); 544 return b[0] == 0x20 && b[1] == 0x01 && b[2] == 0 && b[3] == 0; 545 } 546 547 /** Returns <jk>true</jk> for a local-use NAT64 (<c>64:ff9b:1::/48</c>, RFC 8215) address. */ 548 private static boolean isNat64LocalUse(InetAddress addr) { 549 if (! (addr instanceof Inet6Address)) 550 return false; 551 var b = addr.getAddress(); 552 return b[0] == 0 && b[1] == 0x64 && (b[2] & 0xFF) == 0xFF && (b[3] & 0xFF) == 0x9B && b[4] == 0 && b[5] == 1; 553 } 554 555 /** 556 * Unwraps an IPv6 address that embeds an IPv4 address (IPv4-mapped, IPv4-compatible, IPv4-translated, NAT64 557 * or 6to4) to the embedded {@link Inet4Address}; any other address is returned unchanged. 558 */ 559 @SuppressWarnings({ 560 "java:S3776" // One flat decision table over the IPv6 embedding prefixes; splitting it would scatter the byte-layout rules. 561 }) 562 private static InetAddress unwrapEmbeddedIpv4(InetAddress addr) { 563 if (! (addr instanceof Inet6Address)) 564 return addr; 565 var b = addr.getAddress(); 566 byte[] v4 = null; 567 if (isZero(b, 0, 10)) { 568 // IPv4-mapped ::ffff:a.b.c.d, or IPv4-compatible ::a.b.c.d (but '::' and '::1' keep their IPv6 classification). 569 var mapped = (b[10] & 0xFF) == 0xFF && (b[11] & 0xFF) == 0xFF; 570 var compatible = b[10] == 0 && b[11] == 0 && (b[12] != 0 || b[13] != 0 || b[14] != 0 || (b[15] & 0xFF) > 1); 571 if (mapped || compatible) 572 v4 = new byte[]{b[12], b[13], b[14], b[15]}; 573 } else if (isZero(b, 0, 8) && (b[8] & 0xFF) == 0xFF && (b[9] & 0xFF) == 0xFF && b[10] == 0 && b[11] == 0) { 574 // IPv4-translated (RFC 2765) ::ffff:0:a.b.c.d 575 v4 = new byte[]{b[12], b[13], b[14], b[15]}; 576 } else if (b[0] == 0 && b[1] == 0x64 && (b[2] & 0xFF) == 0xFF && (b[3] & 0xFF) == 0x9B) { 577 // NAT64 well-known prefix 64:ff9b::/96 (RFC 6052). The local-use 64:ff9b:1::/48 is denied before unwrapping. 578 if (isZero(b, 4, 12)) 579 v4 = new byte[]{b[12], b[13], b[14], b[15]}; 580 } else if (b[0] == 0x20 && b[1] == 0x02) { 581 // 6to4 2002::/16: the IPv4 address is in bytes 2-5. 582 v4 = new byte[]{b[2], b[3], b[4], b[5]}; 583 } 584 if (v4 == null) 585 return addr; 586 try { 587 return InetAddress.getByAddress(v4); 588 } catch (UnknownHostException e) { 589 return addr; // HTT: getByAddress with a 4-byte array never throws 590 } 591 } 592 593 // Returns true if bytes [from, to) are all zero. 594 private static boolean isZero(byte[] b, int from, int to) { 595 for (var i = from; i < to; i++) 596 if (b[i] != 0) 597 return false; 598 return true; 599 } 600 601 /** 602 * Resolves {@code host} and returns the first candidate address that is allowed by the default policy 603 * ("pin-on-connect") -- the address the caller should actually connect to. When multiple candidates are 604 * returned (e.g. mixed A/AAAA), the first allowed one wins; a candidate that is itself denied is skipped rather 605 * than rejecting the whole resolution, so a name with one public and one loopback address may still be used. 606 * 607 * @param host The hostname (or IP literal) to resolve and pin. 608 * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped and the first resolved candidate is 609 * returned unconditionally. 610 * @param resolver The address resolver to use. Pass {@link AddressResolver#DEFAULT} for real DNS, or an 611 * injectable stub in tests to simulate rebinding/mixed-family resolution without live DNS. 612 * @return The selected address to connect to. Never <jk>null</jk>. 613 * @throws UnknownHostException If {@code host} cannot be resolved. 614 * @throws IllegalArgumentException If every resolved candidate is denied (and {@code allowPrivateUrls} is 615 * <jk>false</jk>). 616 */ 617 public static InetAddress selectAllowedAddress(String host, boolean allowPrivateUrls, AddressResolver resolver) throws UnknownHostException { 618 return selectAllowedAddress(host, allowPrivateUrls, resolver, AllowedRanges.NONE); 619 } 620 621 /** 622 * Same as {@link #selectAllowedAddress(String, boolean, AddressResolver)}, additionally re-permitting candidates 623 * inside the per-client {@code allowedRanges} (unioned with the {@value #ALLOW_ADDRESS_RANGES_PROPERTY} system property). 624 * 625 * @param host The hostname (or IP literal) to resolve and pin. 626 * @param allowPrivateUrls When <jk>true</jk>, the deny-list check is skipped. 627 * @param resolver The address resolver to use. 628 * @param allowedRanges The per-client narrow allow-list. Must not be <jk>null</jk>. 629 * @return The selected address to connect to. Never <jk>null</jk>. 630 * @throws UnknownHostException If {@code host} cannot be resolved. 631 * @throws IllegalArgumentException If every resolved candidate is denied. 632 */ 633 public static InetAddress selectAllowedAddress(String host, boolean allowPrivateUrls, AddressResolver resolver, AllowedRanges allowedRanges) throws UnknownHostException { 634 var candidates = resolver.resolve(host); 635 if (allowPrivateUrls) 636 return candidates[0]; 637 for (var c : candidates) 638 if (! isDeniedAddress(c, allowedRanges)) 639 return c; 640 throw iaex("No allowed address for host '%s': every resolved candidate is private/loopback/link-local/reserved (set allowPrivateUrls to allow local-dev targets, or list a special-purpose range such as 198.18.0.0/15 in the RestClient.allowAddressRanges system property or the client's allowAddressRanges setting)", host); 641 } 642 643 /** 644 * Resolves a redirect {@code Location} header value against the current request URI, per RFC 7231 ยง7.1.2 645 * (relative locations are resolved against the request URI). 646 * 647 * @param from The current (pre-redirect) request URI. Must be absolute. 648 * @param location The {@code Location} header value. Must not be <jk>null</jk> or blank. 649 * @return The resolved absolute redirect-target URI. 650 * @throws IllegalArgumentException If {@code location} is blank or does not resolve to an absolute URI. 651 */ 652 public static URI resolveRedirectLocation(URI from, String location) { 653 if (ib(location)) 654 throw iaex("Redirect response is missing a Location header"); 655 URI target; 656 try { 657 target = from.resolve(location.trim()); 658 } catch (IllegalArgumentException e) { 659 throw iaex(e, "Redirect response has an invalid Location header: %s", location); 660 } 661 if (! target.isAbsolute()) 662 throw iaex("Redirect Location did not resolve to an absolute URI: %s", location); 663 return target; 664 } 665 666 /** 667 * Returns the URI scheme of a URL (the token before the first {@code :} when it precedes any {@code /}, 668 * {@code ?} or {@code #}), or <jk>null</jk> if the value has no scheme (e.g. a relative path or a value 669 * beginning with a {@code {var}} token). 670 * 671 * @param url The URL to inspect. 672 * @return The scheme, or <jk>null</jk> if none. 673 */ 674 public static String schemeOf(String url) { 675 if (url.isEmpty() || ! Character.isLetter(url.charAt(0))) 676 return null; 677 for (var i = 0; i < url.length(); i++) { 678 var c = url.charAt(i); 679 if (c == ':') 680 return url.substring(0, i); 681 if (c == '/' || c == '?' || c == '#') 682 return null; 683 if (! (Character.isLetterOrDigit(c) || c == '+' || c == '-' || c == '.')) 684 return null; 685 } 686 return null; 687 } 688}