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.petstore.console; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.io.*; 022import java.util.*; 023 024import org.apache.juneau.commons.settings.*; 025import org.apache.juneau.rest.server.filter.*; 026 027import jakarta.servlet.*; 028import jakarta.servlet.http.*; 029 030/** 031 * The {@code /petstore} API's CSRF boundary: a {@link RequestBoundaryFilter} over the framework's {@link CsrfBoundary}, 032 * applied to {@code /petstore} and everything under it. 033 * 034 * <p> 035 * A browser write to the API must come from the page's own origin and carry this boundary's token in the 036 * {@code X-Csrf-Token} header. The token travels in the header only, so every HTML {@code <form>} POST to 037 * the API is refused, the application's own included. Requests are judged on their effective method, so 038 * {@code GET /petstore/pets/1?method=DELETE} counts as a write. Reads are allowed, and so are non-browser writes 039 * (no {@code Origin} and no {@code Sec-Fetch-Site}) that a browser could not have sent cross-site without a CORS 040 * preflight: JSON, XML or UON bodies, and {@code PUT}, {@code DELETE} or {@code PATCH}. A non-browser urlencoded, 041 * multipart or {@code text/plain} {@code POST} is refused. Every refusal names its reason in the 042 * {@value RequestBoundaryFilter#REJECTION_HEADER} response header. 043 * 044 * <h5 class='section'>The token</h5> 045 * <p> 046 * The token is per process ({@link CsrfTokens#perProcess()}): the petstore has no login session, and the 047 * {@code Origin} check does the real work. A restart invalidates the token on every open page, so reload them. A 048 * token that leaks through a page stays valid until the restart. 049 * 050 * <p> 051 * This token is not the console's. The console boundary ({@link PetstoreConsoleBoundary}) has its own, and each 052 * boundary refuses the other's. The console's one write to the API, its photo upload, gets this boundary's token 053 * from the console boundary, which publishes {@link #tokenFor(HttpServletRequest)} under {@link #TOKEN_ATTRIBUTE} 054 * on every console request it allows; the page's script sends it in that header. So a runner must 055 * give the console boundary the same instance it mounts here: a token from any other instance is refused. 056 * 057 * <p> 058 * The console page shell marks a token-bearing page {@code no-store}, and when the response is already committed it 059 * strips only its own token attributes. So on the rare page whose buffer was flushed before the shell ran, this 060 * boundary's token can still be emitted, possibly without {@code no-store}. That is accepted: this token is only 061 * ever published on console pages, which carry the console's token too, so on every normal path the page is already 062 * marked. 063 * 064 * <h5 class='section'>The host</h5> 065 * <p> 066 * The request's own origin is its scheme plus its {@code Host} header; forwarded headers are never read. By default 067 * any {@code Host} is answered, which suits a loopback developer setup. On a host served over plain HTTP, set 068 * {@value #ALLOWED_HOSTS_PROPERTY} (a system property, the environment, or any registered setting source) to the 069 * comma-separated host names the browser uses, without ports. Every request to the API under any other {@code Host}, 070 * reads included, is then answered with {@code 421}: this is the DNS-rebinding defence. The setting is read once, 071 * when the boundary is constructed, and a malformed entry fails construction rather than being ignored. 072 * 073 * <p> 074 * Behind a TLS-terminating proxy the browser's {@code https} origin does not match the {@code http} request the 075 * container sees, so writes are refused. Such a deployment needs the container's forwarded-header support and the 076 * framework boundary's {@link org.apache.juneau.rest.server.filter.CsrfBoundary.OriginSource#CONTAINER CONTAINER} origin source, which this demo does not 077 * configure. 078 * 079 * <h5 class='section'>Example:</h5> 080 * <p> 081 * In a Juneau microservice, contribute a <c>ServletFilterRegistration</c> <c>@Bean</c> just after the console 082 * boundary's, sharing the one instance with it: 083 * <p class='bjava'> 084 * <jk>private final</jk> PetstoreApiBoundary <jf>apiBoundary</jf> = <jk>new</jk> PetstoreApiBoundary(); 085 * 086 * <ja>@Bean</ja> 087 * <jk>public</jk> ServletFilterRegistration apiBoundary() { 088 * <jk>return</jk> ServletFilterRegistration.<jsm>create</jsm>(<jf>apiBoundary</jf>) 089 * .urlPatterns(PetstoreApiBoundary.<jsf>URL_PATTERN</jsf>) 090 * .dispatcherTypes(DispatcherType.<jsf>REQUEST</jsf>) 091 * .order(ServletFilterRegistration.<jsf>HIGHEST_PRECEDENCE</jsf> + 10) 092 * .build(); 093 * } 094 * </p> 095 * <p> 096 * and build the console boundary with {@code new PetstoreConsoleBoundary(apiBoundary)}. Register it for 097 * {@code REQUEST} dispatches only: an internal forward or error dispatch is not a new request from the browser. 098 */ 099public class PetstoreApiBoundary implements Filter { 100 101 /** The covered path: the API resource's mount point. */ 102 public static final String PATH = "/petstore"; 103 104 /** The servlet URL pattern the boundary is registered under. It matches {@link #PATH} itself and every path under it. */ 105 public static final String URL_PATTERN = PATH + "/*"; 106 107 /** 108 * Setting (system property, environment, or any registered source) listing the host names the API answers to: 109 * comma-separated, no ports. Unset or blank, any host is answered. 110 */ 111 public static final String ALLOWED_HOSTS_PROPERTY = "petstore.api.allowedHosts"; 112 113 /** 114 * Request attribute under which an allowed console request carries a {@link String}: a freshly masked token for 115 * this boundary, for a console page to stamp on the form that writes to the API. 116 */ 117 public static final String TOKEN_ATTRIBUTE = "org.apache.juneau.petstore.apiCsrfToken"; 118 119 private final CsrfBoundary boundary; 120 private final RequestBoundaryFilter filter; 121 122 /** 123 * Constructor. 124 * 125 * <p> 126 * Reads {@value #ALLOWED_HOSTS_PROPERTY} and creates the boundary's per-process token. 127 * 128 * @throws IllegalArgumentException If {@value #ALLOWED_HOSTS_PROPERTY} holds an entry that is blank or not a bare 129 * host name. 130 */ 131 public PetstoreApiBoundary() { 132 boundary = CsrfBoundary.create() 133 .tokens(CsrfTokens.perProcess()) 134 .allowedHosts(allowedHosts(Settings.get().get(ALLOWED_HOSTS_PROPERTY).orElse(null))) 135 .build(); 136 filter = new RequestBoundaryFilter(boundary); 137 } 138 139 /** 140 * The host names in an {@value #ALLOWED_HOSTS_PROPERTY} setting. 141 * 142 * @param setting The setting. Can be <jk>null</jk> or blank (not set). 143 * @return The comma-separated entries, trimmed, blank ones included so the boundary refuses them; empty when the 144 * setting is not set. 145 */ 146 private static String[] allowedHosts(String setting) { 147 if (ib(setting)) 148 return new String[0]; 149 return Arrays.stream(setting.split(",", -1)).map(String::trim).toArray(String[]::new); 150 } 151 152 /** 153 * A freshly masked token for a page served to this request to embed. 154 * 155 * <p> 156 * Never creates a session. The value is different on every call, and every value verifies on this instance only. 157 * 158 * @param req The request whose response will embed the token. Must not be <jk>null</jk>. 159 * @return 128 lower-case hex characters. 160 */ 161 public String tokenFor(HttpServletRequest req) { 162 return boundary.tokenFor(req); 163 } 164 165 /** 166 * Applies the boundary: a refused request is answered here and never reaches the API. 167 */ 168 @Override /* Filter */ 169 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { 170 filter.doFilter(req, res, chain); 171 } 172}