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 application.getSecuritySettings().getRandomSupplier().getRandomBase64(NONCE_LENGTH); 082 } 083 084 public CSPHeaderConfiguration blocking() 085 { 086 return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, x -> new CSPHeaderConfiguration()); 087 } 088 089 public CSPHeaderConfiguration reporting() 090 { 091 return configs.computeIfAbsent(CSPHeaderMode.REPORT_ONLY, 092 x -> new CSPHeaderConfiguration()); 093 } 094 095 /** 096 * Sets the creator of nonces. 097 * 098 * @param nonceCreator 099 * The new creator, must not be null. 100 * @return {@code this} for chaining. 101 */ 102 public ContentSecurityPolicySettings setNonceCreator(Supplier<String> nonceCreator) 103 { 104 Args.notNull(nonceCreator, "nonceCreator"); 105 this.nonceCreator = nonceCreator; 106 return this; 107 } 108 109 /** 110 * Sets the predicate that determines which requests must be protected by the CSP. When the 111 * predicate evaluates to false, the request will not be protected. 112 * 113 * @param protectedFilter 114 * The new filter, must not be null. 115 * @return {@code this} for chaining. 116 */ 117 public ContentSecurityPolicySettings setProtectedFilter( 118 Predicate<IRequestHandler> protectedFilter) 119 { 120 Args.notNull(protectedFilter, "protectedFilter"); 121 this.protectedFilter = protectedFilter; 122 return this; 123 } 124 125 /** 126 * Should any request be protected by CSP. 127 * 128 * @param handler 129 * @return <code>true</code> by default for all {@link RenderPageRequestHandler}s 130 * 131 * @see #setProtectedFilter(Predicate) 132 */ 133 protected boolean mustProtectRequest(IRequestHandler handler) 134 { 135 return protectedFilter.test(handler); 136 } 137 138 /** 139 * Returns true if any of the headers includes a directive with a nonce. 140 * 141 * @return If a nonce is used in the CSP. 142 */ 143 public final boolean isNonceEnabled() 144 { 145 return configs.values().stream().anyMatch(CSPHeaderConfiguration::isNonceEnabled); 146 } 147 148 public String getNonce(RequestCycle cycle) 149 { 150 IRequestHandler handler = cycle.getActiveRequestHandler(); 151 152 Page currentPage = IPageRequestHandler.getPage(handler); 153 154 String nonce = cycle.getMetaData(NONCE_KEY); 155 if (nonce == null) 156 { 157 if (currentPage != null) 158 { 159 nonce = currentPage.getMetaData(NONCE_KEY); 160 } 161 if (nonce == null) 162 { 163 nonce = createNonce(); 164 } 165 cycle.setMetaData(NONCE_KEY, nonce); 166 } 167 168 if (currentPage != null) 169 { 170 currentPage.setMetaData(NONCE_KEY, nonce); 171 } 172 173 return nonce; 174 } 175 176 /** 177 * Create a new nonce. 178 * 179 * @return nonce 180 * 181 * @see #setNonceCreator(Supplier) 182 */ 183 protected String createNonce() 184 { 185 return nonceCreator.get(); 186 } 187 188 /** 189 * Returns the CSP configuration per {@link CSPHeaderMode}. 190 * 191 * @return the CSP configuration per {@link CSPHeaderMode}. 192 */ 193 public Map<CSPHeaderMode, CSPHeaderConfiguration> getConfiguration() 194 { 195 return Collections.unmodifiableMap(configs); 196 } 197 198 /** 199 * Enforce CSP settings on an application. 200 * 201 * @param application 202 * application 203 */ 204 public void enforce(WebApplication application) 205 { 206 application.getRequestCycleListeners().add(new CSPRequestCycleListener(this)); 207 application.getHeaderResponseDecorators() 208 .addPreResourceAggregationDecorator(response -> new CSPNonceHeaderResponseDecorator(response, this)); 209 application.mount(new ReportCSPViolationMapper(this)); 210 } 211 212 /** 213 * Is CSP enabled. 214 */ 215 public boolean isEnabled() 216 { 217 return configs.values().stream().anyMatch(CSPHeaderConfiguration::isSet); 218 } 219}