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.wicket.csp; 018 019import java.util.Collections; 020import java.util.EnumMap; 021import java.util.Map; 022import java.util.function.Predicate; 023import java.util.function.Supplier; 024 025import org.apache.wicket.Application; 026import org.apache.wicket.MetaDataKey; 027import org.apache.wicket.Page; 028import org.apache.wicket.core.request.handler.IPageRequestHandler; 029import org.apache.wicket.core.request.handler.RenderPageRequestHandler; 030import org.apache.wicket.protocol.http.WebApplication; 031import org.apache.wicket.request.IRequestHandler; 032import org.apache.wicket.request.cycle.RequestCycle; 033import org.apache.wicket.util.lang.Args; 034 035/** 036 * Build the CSP configuration like this: 037 * 038 * <pre> 039 * {@code 040 * myApplication.getCspSettings().blocking().clear() 041 * .add(CSPDirective.DEFAULT_SRC, CSPDirectiveSrcValue.NONE) 042 * .add(CSPDirective.SCRIPT_SRC, CSPDirectiveSrcValue.SELF) 043 * .add(CSPDirective.IMG_SRC, CSPDirectiveSrcValue.SELF) 044 * .add(CSPDirective.FONT_SRC, CSPDirectiveSrcValue.SELF)); 045 * 046 * myApplication.getCspSettings().reporting().strict(); 047 * } 048 * </pre> 049 * 050 * See {@link CSPHeaderConfiguration} for more details on specifying the configuration. 051 * 052 * @see <a href="https://www.w3.org/TR/CSP2/">https://www.w3.org/TR/CSP2</a> 053 * @see <a href= 054 * "https://developer.mozilla.org/en-US/docs/Web/Security/CSP">https://developer.mozilla.org/en-US/docs/Web/Security/CSP</a> 055 * 056 * @author Sven Haster 057 * @author Emond Papegaaij 058 */ 059public class ContentSecurityPolicySettings 060{ 061 // The number of bytes to use for a nonce, 18 will result in a 24 char nonce. 062 private static final int NONCE_LENGTH = 18; 063 064 public static final MetaDataKey<String> NONCE_KEY = new MetaDataKey<>() 065 { 066 private static final long serialVersionUID = 1L; 067 }; 068 069 private final Map<CSPHeaderMode, CSPHeaderConfiguration> configs = new EnumMap<>( 070 CSPHeaderMode.class); 071 072 private Predicate<IRequestHandler> protectedFilter = RenderPageRequestHandler.class::isInstance; 073 074 private Supplier<String> nonceCreator; 075 076 public ContentSecurityPolicySettings(Application application) 077 { 078 Args.notNull(application, "application"); 079 080 nonceCreator = () -> { 081 return application.getSecuritySettings().getRandomSupplier().getRandomBase64(NONCE_LENGTH); 082 }; 083 } 084 085 public CSPHeaderConfiguration blocking() 086 { 087 return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, x -> new CSPHeaderConfiguration()); 088 } 089 090 public CSPHeaderConfiguration reporting() 091 { 092 return configs.computeIfAbsent(CSPHeaderMode.REPORT_ONLY, 093 x -> new CSPHeaderConfiguration()); 094 } 095 096 /** 097 * Sets the creator of nonces. 098 * 099 * @param nonceCreator 100 * The new creator, must not be null. 101 * @return {@code this} for chaining. 102 */ 103 public ContentSecurityPolicySettings setNonceCreator(Supplier<String> nonceCreator) 104 { 105 Args.notNull(nonceCreator, "nonceCreator"); 106 this.nonceCreator = nonceCreator; 107 return this; 108 } 109 110 /** 111 * Sets the predicate that determines which requests must be protected by the CSP. When the 112 * predicate evaluates to false, the request will not be protected. 113 * 114 * @param protectedFilter 115 * The new filter, must not be null. 116 * @return {@code this} for chaining. 117 */ 118 public ContentSecurityPolicySettings setProtectedFilter( 119 Predicate<IRequestHandler> protectedFilter) 120 { 121 Args.notNull(protectedFilter, "protectedFilter"); 122 this.protectedFilter = protectedFilter; 123 return this; 124 } 125 126 /** 127 * Should any request be protected by CSP. 128 * 129 * @param handler 130 * @return <code>true</code> by default for all {@link RenderPageRequestHandler}s 131 * 132 * @see #setProtectedFilter(Predicate) 133 */ 134 protected boolean mustProtectRequest(IRequestHandler handler) 135 { 136 return protectedFilter.test(handler); 137 } 138 139 /** 140 * Returns true if any of the headers includes a directive with a nonce. 141 * 142 * @return If a nonce is used in the CSP. 143 */ 144 public final boolean isNonceEnabled() 145 { 146 return configs.values().stream().anyMatch(CSPHeaderConfiguration::isNonceEnabled); 147 } 148 149 public String getNonce(RequestCycle cycle) 150 { 151 IRequestHandler handler = cycle.getActiveRequestHandler(); 152 153 Page currentPage = IPageRequestHandler.getPage(handler); 154 155 String nonce = cycle.getMetaData(NONCE_KEY); 156 if (nonce == null) 157 { 158 if (currentPage != null) 159 { 160 nonce = currentPage.getMetaData(NONCE_KEY); 161 } 162 if (nonce == null) 163 { 164 nonce = createNonce(); 165 } 166 cycle.setMetaData(NONCE_KEY, nonce); 167 } 168 169 if (currentPage != null) 170 { 171 currentPage.setMetaData(NONCE_KEY, nonce); 172 } 173 174 return nonce; 175 } 176 177 /** 178 * Create a new nonce. 179 * 180 * @return nonce 181 * 182 * @see #setNonceCreator(Supplier) 183 */ 184 protected String createNonce() 185 { 186 return nonceCreator.get(); 187 } 188 189 /** 190 * Returns the CSP configuration per {@link CSPHeaderMode}. 191 * 192 * @return the CSP configuration per {@link CSPHeaderMode}. 193 */ 194 public Map<CSPHeaderMode, CSPHeaderConfiguration> getConfiguration() 195 { 196 return Collections.unmodifiableMap(configs); 197 } 198 199 /** 200 * Enforce CSP settings on an application. 201 * 202 * @param application 203 * application 204 */ 205 public void enforce(WebApplication application) 206 { 207 application.getRequestCycleListeners().add(new CSPRequestCycleListener(this)); 208 application.getHeaderResponseDecorators() 209 .addPreResourceAggregationDecorator(response -> new CSPNonceHeaderResponseDecorator(response, this)); 210 application.mount(new ReportCSPViolationMapper(this)); 211 } 212 213 /** 214 * Is CSP enabled. 215 */ 216 public boolean isEnabled() 217 { 218 return configs.values().stream().anyMatch(CSPHeaderConfiguration::isSet); 219 } 220}