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.rest.server; 018 019 020// Single-type import, not the usual wildcard: this package declares its own Method type, which would otherwise 021// shadow java.lang.reflect.Method here. 022import java.lang.reflect.Method; 023import java.net.*; 024import java.nio.charset.*; 025import java.util.*; 026import java.util.function.*; 027 028import org.apache.juneau.commons.reflect.*; 029import org.apache.juneau.http.response.*; 030import static org.apache.juneau.commons.utils.Shorts.*; 031 032/** 033 * Which HTTP methods are safe, and the startup check that an operation declared {@link Mutating} is not bound to 034 * one of them. 035 * 036 * <p> 037 * This is the single definition of the safe-method set for this framework. Anything that needs to know whether a 038 * request may change state — notably 039 * {@link org.apache.juneau.rest.server.filter.LoopbackBoundary#isStateChanging(String)}, which delegates here 040 * — reads it from this class, so a security filter and a startup check cannot come to disagree about which 041 * methods are writes. A divergence between those two would be silent and would defeat both. 042 * 043 * <h5 class='section'>The check</h5> 044 * <p> 045 * {@link #check(List)} runs when a resource's operation table is assembled, from 046 * {@link RestOperations#RestOperations(RestOperations.Builder)} — the one constructor every operation table 047 * passes through, so there is no registration to remember and no path around it. It fails the resource when an 048 * operation carrying {@link Mutating @Mutating} resolves to a safe method. 049 * <p> 050 * The resolved method is read from {@link RestOpContext#getHttpMethod()}, which is what dispatch itself uses. That 051 * matters more than it sounds: the method may have been <b>inferred</b> from the Java method name rather than 052 * written down (see {@link RestOp#method()}), and a check that re-derived it independently could disagree with 053 * dispatch about which operation is bound to what. A security-relevant check that disagrees with the router is 054 * worse than none. 055 * <p> 056 * A wildcard operation ({@code @RestOp(method="*")}) matches {@code GET} among everything else, so it is a 057 * contradiction too and is reported as one. 058 * <p> 059 * The same check fails an operation declared {@link RestOp#read() read=true} that is bound to a method defined as 060 * unsafe, to {@code "*"} or to {@code "RRPC"}, or that is also {@link Mutating @Mutating}. A read can be reached by 061 * overriding a safe request, so a misdeclared read would be a write behind a plain link. 062 * 063 * <h5 class='section'>Why the rule is a declared contradiction rather than a detected one</h5> 064 * <p> 065 * See {@link Mutating}, which records why inferring "this handler mutates" is not soundly possible and why a check 066 * that can wrongly refuse to start an application is worse than no check at all. The short form: this class only 067 * ever reports a contradiction the developer wrote down, so it cannot produce a false positive, and it cannot catch 068 * a handler that mutates and says nothing. 069 * 070 * <h5 class='section'>See Also:</h5><ul> 071 * <li class='ja'>{@link Mutating} 072 * <li class='jc'>{@link org.apache.juneau.rest.server.filter.LoopbackBoundary} 073 * </ul> 074 * 075 * @since 10.0.0 076 */ 077public class MethodSafety { 078 079 /** 080 * The request methods RFC 9110 defines as safe. 081 * 082 * <p> 083 * A method absent from this set — including one this framework does not recognize — is treated as 084 * state-changing, so an unusual or future method fails closed rather than skipping write checks. 085 */ 086 private static final Set<String> SAFE_METHODS = Set.of("GET", "HEAD", "OPTIONS", "TRACE"); 087 088 /** The standard methods defined as unsafe, which an operation can never declare a read. */ 089 private static final Set<String> UNSAFE_METHODS = Set.of("POST", "PUT", "PATCH", "DELETE", "CONNECT"); 090 091 /** The query parameter that can claim a request's method. */ 092 private static final String OVERRIDE_PARAM = "method"; 093 094 /** The headers that can claim a request's method. */ 095 private static final List<String> OVERRIDE_HEADERS = List.of("X-Method", "X-HTTP-Method-Override", "X-HTTP-Method", "X-Method-Override"); 096 097 /** 098 * Constructor. 099 */ 100 protected MethodSafety() {} 101 102 /** 103 * Whether {@code method} is defined as safe, and therefore promises not to change state. 104 * 105 * @param method The request method. Can be <jk>null</jk>, which is not safe. 106 * @return <jk>true</jk> if the method is one of {@code GET}, {@code HEAD}, {@code OPTIONS} or {@code TRACE}. 107 */ 108 public static boolean isSafe(String method) { 109 return method != null && SAFE_METHODS.contains(method.toUpperCase(Locale.ROOT)); 110 } 111 112 /** 113 * The method a request is judged on by a write-protection check. 114 * 115 * <p> 116 * A request is judged on the method it <b>claims</b>, not only on its request line. A {@code method} query 117 * parameter, or an {@code X-Method}, {@code X-HTTP-Method-Override}, {@code X-HTTP-Method} or 118 * {@code X-Method-Override} header, naming anything other than a safe method makes the request a write, whether or 119 * not the resource it reaches is configured to honour that override. A check that followed dispatch's override 120 * configuration instead would open a hole the day a resource's allowed overrides changed. 121 * 122 * <p> 123 * The rules fail closed: 124 * <ul> 125 * <li>The request-line method is the floor. An unsafe request line is returned (upper-cased); an override can raise a 126 * request's severity but never lower it, so {@code POST ?method=GET} stays {@code POST}. 127 * <li>Any unsafe claim wins, so {@code ?method=GET&method=DELETE} is {@code DELETE}. 128 * <li>An unknown method is unsafe, so {@code ?method=FROB} is judged as a write. 129 * <li>A claim with a malformed percent-escape is judged on its raw text, which is never a safe method name. 130 * <li>A blank claim is ignored. 131 * </ul> 132 * 133 * @param method The request line's method. Can be <jk>null</jk>, which is returned as <jk>null</jk> (unsafe). 134 * @param rawQuery The raw, undecoded query string. Can be <jk>null</jk>. 135 * @param headers Looks up a request header by name, returning <jk>null</jk> when absent. Must not be <jk>null</jk>. 136 * @return The upper-cased request-line method when it is not safe, else the first unsafe claim (upper-cased), else 137 * the upper-cased request-line method. 138 */ 139 public static String effectiveMethod(String method, String rawQuery, UnaryOperator<String> headers) { 140 reqnn("headers", headers); 141 var m = method == null ? null : method.toUpperCase(Locale.ROOT); 142 if (! isSafe(m)) 143 return m; 144 for (var claim : queryClaims(rawQuery)) { 145 var c = unsafeClaim(claim); 146 if (c != null) 147 return c; 148 } 149 for (var h : OVERRIDE_HEADERS) { 150 var c = unsafeClaim(headers.apply(h)); 151 if (c != null) 152 return c; 153 } 154 return m; 155 } 156 157 /** 158 * The names of the request headers that can claim a request's method, in precedence order. 159 * 160 * <p> 161 * The order is part of the contract: dispatch honours the first admitted header in this order. 162 * 163 * @return An immutable list of header names. Never <jk>null</jk>. 164 */ 165 public static List<String> overrideHeaders() { 166 return OVERRIDE_HEADERS; 167 } 168 169 /** 170 * Every value of the {@code method} query parameter, in order. 171 * 172 * <p> 173 * The one parser shared by the write-protection filters and by dispatch, so they cannot disagree about what a query 174 * string claims. The raw query is split on {@code &}, each name and value is percent-decoded, and the name is matched 175 * case-insensitively. Values are returned untrimmed. A malformed percent-escape leaves the raw text. 176 * 177 * @param rawQuery The raw, undecoded query string. Can be <jk>null</jk>. 178 * @return The decoded values, in order. Never <jk>null</jk>; empty when there is no claim. 179 */ 180 public static List<String> queryClaims(String rawQuery) { 181 if (rawQuery == null) 182 return List.of(); 183 var l = new ArrayList<String>(); 184 for (var pair : rawQuery.split("&")) { 185 var i = pair.indexOf('='); 186 if (i > 0 && OVERRIDE_PARAM.equalsIgnoreCase(decode(pair.substring(0, i)))) 187 l.add(decode(pair.substring(i + 1))); 188 } 189 return List.copyOf(l); 190 } 191 192 /** The claimed method upper-cased when it is unsafe, else <jk>null</jk> (absent, blank or safe). */ 193 private static String unsafeClaim(String claim) { 194 if (ib(claim)) 195 return null; 196 var c = claim.trim().toUpperCase(Locale.ROOT); 197 return isSafe(c) ? null : c; 198 } 199 200 /** Decodes a query-string component; a malformed escape leaves the raw text, which is then judged as is. */ 201 private static String decode(String s) { 202 try { 203 return URLDecoder.decode(s, StandardCharsets.UTF_8); 204 } catch (IllegalArgumentException e) { 205 return s; 206 } 207 } 208 209 /** 210 * Fails when any operation declared {@link Mutating @Mutating} is bound to a safe method, or any operation 211 * declared {@link RestOp#read() read=true} is bound to a method that cannot be a read. 212 * 213 * @param ops The operations of one resource. Must not be <jk>null</jk>. 214 * @throws InternalServerError If a mutating operation is bound to a safe method, or a read declaration is 215 * misplaced or contradicts the operation's method or a {@link Mutating @Mutating} declaration. 216 */ 217 public static void check(List<RestOpContext> ops) { 218 reqnn("ops", ops); 219 for (var op : ops) 220 checkOperation(op.getHttpMethod(), op.getJavaMethod(), op.isRead()); 221 } 222 223 /** 224 * Fails when {@code javaMethod} is declared {@link Mutating @Mutating} and {@code httpMethod} is safe. 225 * 226 * <p> 227 * The per-operation half of {@link #check(List)}, taking the two facts it compares rather than a built 228 * {@link RestOpContext}, so the rule can be exercised directly. 229 * 230 * @param httpMethod The resolved HTTP method, as dispatch sees it. Can be <jk>null</jk>, which is not safe. 231 * {@code "*"} matches every method and is therefore treated as including a safe one. 232 * @param javaMethod The Java method implementing the operation. Must not be <jk>null</jk>. 233 * @throws InternalServerError If {@code javaMethod} is declared mutating and {@code httpMethod} is safe. 234 */ 235 public static void checkOperation(String httpMethod, Method javaMethod) { 236 checkOperation(httpMethod, javaMethod, false); 237 } 238 239 /** 240 * Fails when {@code javaMethod} is declared {@link Mutating @Mutating} and {@code httpMethod} is safe, or when the 241 * operation is declared {@link RestOp#read() read=true} and {@code httpMethod} cannot be a read. 242 * 243 * <p> 244 * A read declaration is refused on the methods defined as unsafe ({@code POST}, {@code PUT}, {@code PATCH}, 245 * {@code DELETE}, {@code CONNECT}), on {@code "*"} (which answers {@code DELETE} too), on {@code "RRPC"} (which 246 * dispatches arbitrary interface methods), and together with {@link Mutating @Mutating}. It is accepted on a 247 * custom method, and is redundant on a safe one. 248 * 249 * @param httpMethod The resolved HTTP method, as dispatch sees it. Can be <jk>null</jk>, which is not safe. 250 * {@code "*"} matches every method and is therefore treated as including a safe one. 251 * @param javaMethod The Java method implementing the operation. Must not be <jk>null</jk>. 252 * @param read Whether the operation is declared {@link RestOp#read() read=true}. 253 * @throws InternalServerError If the declarations contradict each other or the method. 254 * @since 10.0.0 255 */ 256 public static void checkOperation(String httpMethod, Method javaMethod, boolean read) { 257 reqnn("javaMethod", javaMethod); 258 259 // Resolved through MethodInfo rather than Method.getAnnotation, so that an operation inheriting its 260 // declaration from a superclass or interface method is seen. Method.getAnnotation does not walk overrides, 261 // which would let an inherited @Mutating go unchecked; MethodInfo walks matching methods child-to-parent, 262 // the same way the framework resolves every other method annotation. 263 var mutating = MethodInfo.of(javaMethod.getDeclaringClass(), javaMethod).getAnnotations(Mutating.class) 264 .findFirst().map(AnnotationInfo::inner).orElse(null); 265 if (read) 266 checkRead(httpMethod, javaMethod, nn(mutating)); 267 if (mutating == null) 268 return; 269 270 // A wildcard operation answers GET along with everything else, so it is bound to a safe method too. 271 var wildcard = eq(httpMethod, "*"); 272 if (! wildcard && ! isSafe(httpMethod)) 273 return; 274 275 var what = mutating.value().isBlank() ? "" : " It changes: " + mutating.value() + "."; 276 throw new InternalServerError( 277 "Operation '" + javaMethod.getDeclaringClass().getSimpleName() + "." + javaMethod.getName() 278 + "' is annotated @Mutating but is bound to " 279 + (wildcard ? "every method, including safe ones" : "'" + httpMethod + "', which is a safe method") 280 + "." + what 281 + " A safe method promises not to change state, and CSRF and origin checks are applied only to methods" 282 + " that are not safe, so this operation would be reachable without them." 283 + " Bind it to POST/PUT/PATCH/DELETE, or remove @Mutating if it does not actually change state." 284 + " Note that @RestOp with no method= infers the method from the Java method name and defaults to GET."); 285 } 286 287 /** Fails when a read declaration contradicts the operation's method or a {@link Mutating @Mutating} declaration. */ 288 private static void checkRead(String httpMethod, Method javaMethod, boolean mutating) { 289 // Normalized as the operation table does, so a direct caller passing "rrpc" or "METHOD" gets the same verdict. 290 var m = httpMethod == null ? null : ucr(httpMethod); 291 if (eq(m, "METHOD")) 292 m = "*"; 293 String why; 294 if (mutating) 295 why = "is also annotated @Mutating, which declares the opposite"; 296 else if (eq(m, "*")) 297 why = "is bound to every method, including unsafe ones such as DELETE"; 298 else if (eq(m, "RRPC")) 299 why = "is a remote-proxy (RRPC) operation, which dispatches arbitrary interface methods"; 300 else if (nn(m) && UNSAFE_METHODS.contains(m)) 301 why = "is bound to '" + m + "', which is defined as unsafe"; 302 else 303 return; 304 throw new InternalServerError( 305 "Operation '" + javaMethod.getDeclaringClass().getSimpleName() + "." + javaMethod.getName() 306 + "' is declared @RestOp(read=true) but " + why + "." 307 + " A read can be reached by overriding a GET, so declaring a write a read would let a plain link change state." 308 + " Remove read=true, or bind the operation to a custom method that does not change state." 309 + " Note that @RestOp with no method= infers the method from the Java method name."); 310 } 311}