This is an automated email from the ASF dual-hosted git repository. papegaaij pushed a commit to branch csp in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 06eced185b27894f95ad8b156683bc6c6599b4dc Author: Emond Papegaaij <[email protected]> AuthorDate: Thu Jan 16 22:10:44 2020 +0100 WICKET-6727: more refactoring of the CSP API --- .../java/org/apache/wicket/csp/CSPDirective.java | 191 +++++++++ .../wicket/csp/CSPDirectiveSandboxValue.java | 49 +++ .../apache/wicket/csp/CSPDirectiveSrcValue.java | 58 +++ .../apache/wicket/csp/CSPHeaderConfiguration.java | 143 +++++++ .../java/org/apache/wicket/csp/CSPHeaderMode.java | 45 +++ .../java/org/apache/wicket/csp/CSPRenderable.java | 42 ++ .../wicket/csp/CSPSettingRequestCycleListener.java | 441 ++------------------- .../org/apache/wicket/csp/FixedCSPDirective.java | 50 +++ .../csp/CSPSettingRequestCycleListenerTest.java | 96 +++-- 9 files changed, 665 insertions(+), 450 deletions(-) diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirective.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirective.java new file mode 100644 index 0000000..1ea52c1 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirective.java @@ -0,0 +1,191 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.List; + +import org.apache.wicket.util.string.Strings; + +/** + * An enum holding the possible CSP Directives. Via the + * {@link #checkValueForDirective(CSPRenderable, List)}-method, new values can be verified before + * being added to the list of values for a directive. + */ +public enum CSPDirective +{ + DEFAULT_SRC("default-src"), + SCRIPT_SRC("script-src"), + STYLE_SRC("style-src"), + IMG_SRC("img-src"), + CONNECT_SRC("connect-src"), + FONT_SRC("font-src"), + OBJECT_SRC("object-src"), + MANIFEST_SRC("manifest-src"), + MEDIA_SRC("media-src"), + CHILD_SRC("child-src"), + FRAME_ANCESTORS("frame-ancestors"), + @Deprecated + /** @deprecated Gebruik CHILD-SRC, deze zet ook automatisch FRAME-SRC. */ + FRAME_SRC("frame-src"), + SANDBOX("sandbox") + { + @Override + public void checkValueForDirective(CSPRenderable value, + List<CSPRenderable> existingDirectiveValues) + { + if (!existingDirectiveValues.isEmpty()) + { + if (CSPDirectiveSandboxValue.EMPTY.equals(value)) + { + throw new IllegalArgumentException( + "A sandbox directive can't contain an empty string if it already contains other values "); + } + if (existingDirectiveValues.contains(CSPDirectiveSandboxValue.EMPTY)) + { + throw new IllegalArgumentException( + "A sandbox directive can't contain other values if it already contains an empty string"); + } + } + + if (!(value instanceof CSPDirectiveSandboxValue)) + { + throw new IllegalArgumentException( + "A sandbox directive can only contain values from CSPDirectiveSandboxValue or be empty"); + } + } + }, + REPORT_URI("report-uri") + { + @Override + public void checkValueForDirective(CSPRenderable value, + List<CSPRenderable> existingDirectiveValues) + { + if (!existingDirectiveValues.isEmpty()) + { + throw new IllegalArgumentException( + "A report-uri directive can only contain one uri"); + } + if (!(value instanceof FixedCSPDirective)) + { + throw new IllegalArgumentException( + "A report-uri directive can only contain an URI"); + } + try + { + new URI(value.render(null, null)); + } + catch (URISyntaxException urise) + { + throw new IllegalArgumentException("Illegal URI for report-uri directive", urise); + } + } + }; + + private String value; + + private CSPDirective(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + + /** + * Check if {@code value} can be added to the list of other values. + * + * @param value + * The value to add. + * @param existingDirectiveValues + * The other values. + * @throws IllegalArgumentException + * if the given value is invalid. + */ + public void checkValueForDirective(CSPRenderable value, + List<CSPRenderable> existingDirectiveValues) + { + if (!existingDirectiveValues.isEmpty()) + { + if (CSPDirectiveSrcValue.WILDCARD.equals(value) + || CSPDirectiveSrcValue.NONE.equals(value)) + { + throw new IllegalArgumentException( + "A -src directive can't contain an * or a 'none' if it already contains other values "); + } + if (existingDirectiveValues.contains(CSPDirectiveSrcValue.WILDCARD) + || existingDirectiveValues.contains(CSPDirectiveSrcValue.NONE)) + { + throw new IllegalArgumentException( + "A -src directive can't contain other values if it already contains an * or a 'none'"); + } + } + + if (value instanceof CSPDirectiveSrcValue) + { + return; + } + + if (value instanceof CSPDirectiveSandboxValue) + { + throw new IllegalArgumentException( + "A -src directive can't contain any of the sandbox directive values"); + } + + String strValue = value.render(null, null); + if ("data:".equals(strValue) || "https:".equals(strValue)) + { + return; + } + + // strip off "*." so "*.example.com" becomes "example.com" and we can check if + // it + // is a valid uri + if (strValue.startsWith("*.")) + { + strValue = strValue.substring(2); + } + + try + { + new URI(strValue); + } + catch (URISyntaxException urise) + { + throw new IllegalArgumentException("Illegal URI for -src directive", urise); + } + } + + /** + * @return The CSPDirective constant whose value-parameter equals the input-parameter or + * {@code null} if none can be found. + */ + public static CSPDirective fromValue(String value) + { + if (Strings.isEmpty(value)) + return null; + for (int i = 0; i < values().length; i++) + { + if (value.equals(values()[i].getValue())) + return values()[i]; + } + return null; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSandboxValue.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSandboxValue.java new file mode 100644 index 0000000..8164b31 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSandboxValue.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import org.apache.wicket.request.cycle.RequestCycle; + +/** + * An enum representing the only possible values for the sandbox directive + */ +public enum CSPDirectiveSandboxValue implements CSPRenderable +{ + ALLOW_FORMS("allow-forms"), + ALLOW_SAME_ORIGIN("allow-same-origin"), + ALLOW_SCRIPTS("allow-scripts"), + ALLOW_TOP_NAVIGATION("allow-top-navigation"), + EMPTY(""); + + private String value; + + private CSPDirectiveSandboxValue(String value) + { + this.value = value; + } + + public String getValue() + { + return value; + } + + @Override + public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) + { + return value; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSrcValue.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSrcValue.java new file mode 100644 index 0000000..b4a06db --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPDirectiveSrcValue.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import org.apache.wicket.request.cycle.RequestCycle; + +/** + * An enum holding the default values for -src directives including the mandatory single quotes + */ +public enum CSPDirectiveSrcValue implements CSPRenderable +{ + NONE("'none'"), + WILDCARD("*"), + SELF("'self'"), + UNSAFE_INLINE("'unsafe-inline'"), + UNSAFE_EVAL("'unsafe-eval'"), + STRICT_DYNAMIC("'strict-dynamic'"), + NONCE("'nonce-%1$s'") + { + @Override + public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) + { + return String.format(getValue(), listener.getNonce(cycle)); + } + }; + + private String value; + + private CSPDirectiveSrcValue(String value) + { + this.value = value; + } + + @Override + public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) + { + return value; + } + + public String getValue() + { + return value; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderConfiguration.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderConfiguration.java new file mode 100644 index 0000000..1abd1a5 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderConfiguration.java @@ -0,0 +1,143 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.wicket.request.cycle.RequestCycle; + +/** + * + * @author papegaaij + */ +public class CSPHeaderConfiguration +{ + private Map<CSPDirective, List<CSPRenderable>> directives = new EnumMap<>(CSPDirective.class); + + private boolean addLegacyHeaders = false; + + public CSPHeaderConfiguration() + { + } + + /** + * True when legacy headers should be added. + * + * @return True when legacy headers should be added. + */ + public boolean isAddLegacyHeaders() + { + return addLegacyHeaders; + } + + /** + * Enable legacy {@code X-Content-Security-Policy} headers for older browsers, such as IE. + * + * @param addLegacyHeaders + * True when the legacy headers should be added. + * @return {@code this} for chaining + */ + public CSPHeaderConfiguration setAddLegacyHeaders(boolean addLegacyHeaders) + { + this.addLegacyHeaders = addLegacyHeaders; + return this; + } + + /** + * Adds the given values to the CSP directive on this configuraiton. + * + * @param directive + * The directive to add the values to. + * @param values + * The values to add. + */ + public CSPHeaderConfiguration addDirective(CSPDirective directive, CSPRenderable... values) + { + for (CSPRenderable value : values) + { + doAddDirective(directive, value); + } + return this; + } + + /** + * Adds a free-form value to a directive for the CSP header. This is primarily meant to used for + * URIs. + * + * @param directive + * The directive to add the values to. + * @param values + * The values to add. + */ + public CSPHeaderConfiguration addDirective(CSPDirective directive, String... values) + { + for (String value : values) + { + doAddDirective(directive, new FixedCSPDirective(value)); + } + return this; + } + + /** + * @return true if this {@code CSPHeaderConfiguration} has any directives configured. + */ + public boolean isSet() + { + return !directives.isEmpty(); + } + + @SuppressWarnings("deprecation") + private CSPHeaderConfiguration doAddDirective(CSPDirective directive, CSPRenderable value) + { + // Add backwards compatible frame-src + // see http://caniuse.com/#feat=contentsecuritypolicy2 + if (CSPDirective.CHILD_SRC.equals(directive)) + { + doAddDirective(CSPDirective.FRAME_SRC, value); + } + List<CSPRenderable> values = directives.computeIfAbsent(directive, x -> new ArrayList<>()); + directive.checkValueForDirective(value, values); + values.add(value); + return this; + } + + /** + * Renders this {@code CSPHeaderConfiguration} into an HTTP header. The returned String will be + * in the form {@code "key1 value1a value1b; key2 value2a; key3 value3a value3b value3c"}. + * + * @param listener + * The {@link CSPSettingRequestCycleListener} that renders the header. + * @param cycle + * The current {@link RequestCycle}. + * @return the rendered header. + */ + public String renderHeaderValue(CSPSettingRequestCycleListener listener, RequestCycle cycle) + { + return directives.entrySet() + .stream() + .map(e -> e.getKey().getValue() + " " + + e.getValue() + .stream() + .map(r -> r.render(listener, cycle)) + .collect(Collectors.joining(" "))) + .collect(Collectors.joining("; ")); + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderMode.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderMode.java new file mode 100644 index 0000000..48a3a95 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPHeaderMode.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +/** + * Defines the possible headers for a CSP directive. + * + * @author papegaaij + */ +public enum CSPHeaderMode +{ + BLOCKING("Content-Security-Policy"), + REPORT_ONLY("Content-Security-Policy-Report-Only"); + + private final String header; + + private CSPHeaderMode(String header) + { + this.header = header; + } + + public String getHeader() + { + return header; + } + + public String getLegacyHeader() + { + return "X-" + getHeader(); + } +} \ No newline at end of file diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPRenderable.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPRenderable.java new file mode 100644 index 0000000..05badc9 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPRenderable.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import org.apache.wicket.request.cycle.RequestCycle; + +/** + * {@code CSPRenderable} describes a directive that is part of a Content-Security-Policy (CSP in + * short). Most directives are predefined in enums. + * + * @author papegaaij + * @see CSPDirectiveSrcValue + * @see CSPDirectiveSandboxValue + * @see FixedCSPDirective + */ +public interface CSPRenderable +{ + /** + * Renders the value that should be put in the CSP header. + * + * @param listener + * The {@link CSPSettingRequestCycleListener} that renders this value. + * @param cycle + * The current {@link RequestCycle}. + * @return The rendered value. + */ + public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle); +} \ No newline at end of file diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java b/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java index d688990..85cff77 100644 --- a/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java +++ b/wicket-core/src/main/java/org/apache/wicket/csp/CSPSettingRequestCycleListener.java @@ -16,23 +16,19 @@ */ package org.apache.wicket.csp; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Base64; -import java.util.EnumMap; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; +import org.apache.wicket.Application; import org.apache.wicket.MetaDataKey; +import org.apache.wicket.core.request.handler.BufferedResponseRequestHandler; +import org.apache.wicket.core.request.handler.IPageClassRequestHandler; import org.apache.wicket.request.IRequestHandler; +import org.apache.wicket.request.IRequestHandlerDelegate; import org.apache.wicket.request.cycle.IRequestCycleListener; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.http.WebResponse; -import org.apache.wicket.util.string.Strings; +import org.apache.wicket.util.lang.Args; /** * An {@link IRequestCycleListener} that adds {@code Content-Security-Policy} and/or @@ -78,425 +74,54 @@ public class CSPSettingRequestCycleListener implements IRequestCycleListener private static final long serialVersionUID = 1L; }; - public static interface CSPRenderable - { - public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle); - } + private final Application application; + + private Map<CSPHeaderMode, CSPHeaderConfiguration> configs = new HashMap<>(); - private static final class FixedCSPDirective implements CSPRenderable + public CSPSettingRequestCycleListener(Application application) { - private String value; - - public FixedCSPDirective(String value) - { - if (Strings.isEmpty(value)) - throw new IllegalArgumentException( - "CSP directive cannot have empty or null values"); - this.value = value; - } - - @Override - public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) - { - return value; - } + this.application = Args.notNull(application, "application"); } - /** - * An enum holding the default values for -src directives including the mandatory single quotes - */ - public enum CSPDirectiveSrcValue implements CSPRenderable + public CSPHeaderConfiguration blocking() { - NONE("'none'"), - WILDCARD("*"), - SELF("'self'"), - UNSAFE_INLINE("'unsafe-inline'"), - UNSAFE_EVAL("'unsafe-eval'"), - STRICT_DYNAMIC("'strict-dynamic'"), - NONCE("'nonce-%1$s'") - { - @Override - public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) - { - return String.format(getValue(), listener.getNonce(cycle)); - } - }; - - private String value; - - private CSPDirectiveSrcValue(String value) - { - this.value = value; - } - - @Override - public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) - { - return value; - } - - public String getValue() - { - return value; - } + return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, x -> new CSPHeaderConfiguration()); } - /** - * An enum representing the only possible values for the sandbox directive - */ - public enum CSPDirectiveSandboxValue implements CSPRenderable + public CSPHeaderConfiguration reporting() { - ALLOW_FORMS("allow-forms"), - ALLOW_SAME_ORIGIN("allow-same-origin"), - ALLOW_SCRIPTS("allow-scripts"), - ALLOW_TOP_NAVIGATION("allow-top-navigation"), - EMPTY(""); - - private String value; - - private CSPDirectiveSandboxValue(String value) - { - this.value = value; - } - - public String getValue() - { - return value; - } - - @Override - public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) - { - return value; - } + return configs.computeIfAbsent(CSPHeaderMode.REPORT_ONLY, x -> new CSPHeaderConfiguration()); } - /** An enum holding the possible CSP Directives */ - public enum CSPDirective + protected boolean mustProtect(IRequestHandler handler) { - DEFAULT_SRC("default-src"), - SCRIPT_SRC("script-src"), - STYLE_SRC("style-src"), - IMG_SRC("img-src"), - CONNECT_SRC("connect-src"), - FONT_SRC("font-src"), - OBJECT_SRC("object-src"), - MANIFEST_SRC("manifest-src"), - MEDIA_SRC("media-src"), - CHILD_SRC("child-src"), - FRAME_ANCESTORS("frame-ancestors"), - @Deprecated - /** @deprecated Gebruik CHILD-SRC, deze zet ook automatisch FRAME-SRC. */ - FRAME_SRC("frame-src"), - SANDBOX("sandbox") - { - @Override - protected void checkValueForDirective(CSPRenderable value, - List<CSPRenderable> existingDirectiveValues) - { - if (!existingDirectiveValues.isEmpty()) - { - if (CSPDirectiveSandboxValue.EMPTY.equals(value)) - { - throw new IllegalArgumentException( - "A sandbox directive can't contain an empty string if it already contains other values "); - } - if (existingDirectiveValues.contains(CSPDirectiveSandboxValue.EMPTY)) - { - throw new IllegalArgumentException( - "A sandbox directive can't contain other values if it already contains an empty string"); - } - } - - if (!(value instanceof CSPDirectiveSandboxValue)) - { - throw new IllegalArgumentException( - "A sandbox directive can only contain values from CSPDirectiveSandboxValue or be empty"); - } - } - }, - REPORT_URI("report-uri") - { - @Override - protected void checkValueForDirective(CSPRenderable value, - List<CSPRenderable> existingDirectiveValues) - { - if (!existingDirectiveValues.isEmpty()) - { - throw new IllegalArgumentException( - "A report-uri directive can only contain one uri"); - } - if (!(value instanceof FixedCSPDirective)) - { - throw new IllegalArgumentException( - "A report-uri directive can only contain an URI"); - } - try - { - new URI(value.render(null, null)); - } - catch (URISyntaxException urise) - { - throw new IllegalArgumentException("Illegal URI for report-uri directive", - urise); - } - } - }; - - private String value; - - private CSPDirective(String value) - { - this.value = value; - } - - public String getValue() + if (handler instanceof IRequestHandlerDelegate) + return mustProtect(((IRequestHandlerDelegate) handler).getDelegateHandler()); + if (handler instanceof IPageClassRequestHandler) { - return value; - } - - protected void checkValueForDirective(CSPRenderable value, - List<CSPRenderable> existingDirectiveValues) - { - if (!existingDirectiveValues.isEmpty()) - { - if (CSPDirectiveSrcValue.WILDCARD.equals(value) - || CSPDirectiveSrcValue.NONE.equals(value)) - { - throw new IllegalArgumentException( - "A -src directive can't contain an * or a 'none' if it already contains other values "); - } - if (existingDirectiveValues.contains(CSPDirectiveSrcValue.WILDCARD) - || existingDirectiveValues.contains(CSPDirectiveSrcValue.NONE)) - { - throw new IllegalArgumentException( - "A -src directive can't contain other values if it already contains an * or a 'none'"); - } - } - - if (value instanceof CSPDirectiveSrcValue) - { - return; - } - - if (value instanceof CSPDirectiveSandboxValue) - { - throw new IllegalArgumentException( - "A -src directive can't contain any of the sandbox directive values"); - } - - String strValue = value.render(null, null); - if ("data:".equals(strValue) || "https:".equals(strValue)) - { - return; - } - - // strip off "*." so "*.example.com" becomes "example.com" and we can check if - // it - // is a valid uri - if (strValue.startsWith("*.")) - { - strValue = strValue.substring(2); - } - - try - { - new URI(strValue); - } - catch (URISyntaxException urise) - { - throw new IllegalArgumentException("Illegal URI for -src directive", urise); - } - } - - /** - * @return The CSPDirective constant whose value-parameter equals the input-parameter or - * {@code null} if none can be found. - */ - public static CSPDirective fromValue(String value) - { - if (Strings.isEmpty(value)) - return null; - for (int i = 0; i < values().length; i++) - { - if (value.equals(values()[i].getValue())) - return values()[i]; - } - return null; + return mustProtectPageRequest((IPageClassRequestHandler) handler); } + return !(handler instanceof BufferedResponseRequestHandler); } - - private enum CSPHeaderMode - { - BLOCKING("Content-Security-Policy"), - REPORT_ONLY("Content-Security-Policy-Report-Only"); - - private final String header; - - private CSPHeaderMode(String header) - { - this.header = header; - } - - public String getHeader() - { - return header; - } - - public String getLegacyHeader() - { - return "X-" + getHeader(); - } - } - - public class CSPConfiguration - { - private CSPHeaderMode mode; - - private Map<CSPDirective, List<CSPRenderable>> directives = - new EnumMap<>(CSPDirective.class); - - private boolean addLegacyHeaders = false; - - private CSPConfiguration(CSPHeaderMode mode) - { - this.mode = mode; - } - - public CSPHeaderMode getMode() - { - return mode; - } - - /** - * True when legacy headers should be added. - * - * @return True when legacy headers should be added. - */ - public boolean isAddLegacyHeaders() - { - return addLegacyHeaders; - } - - /** - * Enable legacy {@code X-Content-Security-Policy} headers for older browsers, such as IE. - * - * @param addLegacyHeaders - * True when the legacy headers should be added. - * @return {@code this} for chaining - */ - public CSPConfiguration setAddLegacyHeaders(boolean addLegacyHeaders) - { - this.addLegacyHeaders = addLegacyHeaders; - return this; - } - - public CSPConfiguration addDirective(CSPDirective directive, CSPDirectiveSrcValue... values) - { - for (CSPDirectiveSrcValue value : values) - { - doAddDirective(directive, value); - } - return this; - } - - /** - * Adds any of the default values to the sandbox directive for the 'blocking' CSP header. - * Use {@link #addBlockingDirective(CSPDirective, String...)} with the sandbox - * {@link CSPDirective} and a single empty string (<em>not</em> {@code null}) to add the - * empty sandbox directive. - */ - public CSPConfiguration addDirective(CSPDirective sandboxDirective, - CSPDirectiveSandboxValue... values) - { - for (CSPDirectiveSandboxValue value : values) - { - doAddDirective(sandboxDirective, value); - } - return this; - } - - /** - * Adds any value to a directive for the 'blocking' CSP header. Use - * {@link #addBlockingDirective(CSPDirective, CSPDirectiveSandboxValue...)} and - * {@link #addBlockingDirective(CSPDirective, CSPDirectiveSrcValue...)} for the default - * values for the sandbox and -src directives. - */ - public CSPConfiguration addDirective(CSPDirective directive, String... values) - { - for (String value : values) - { - doAddDirective(directive, new FixedCSPDirective(value)); - } - return this; - } - - public boolean isSet() - { - return !directives.isEmpty(); - } - - private CSPConfiguration doAddDirective(CSPDirective directive, CSPRenderable value) - { - // Add backwards compatible frame-src - // see http://caniuse.com/#feat=contentsecuritypolicy2 - if (CSPDirective.CHILD_SRC.equals(directive)) - { - doAddDirective(CSPDirective.FRAME_SRC, value); - } - List<CSPRenderable> values = - directives.computeIfAbsent(directive, x -> new ArrayList<>()); - directive.checkValueForDirective(value, values); - values.add(value); - return this; - } - - // @returns "key1 value1a value1b; key2 value2a; key3 value3a value3b value3c" - public String renderHeaderValue(RequestCycle cycle) - { - return directives.entrySet() - .stream() - .map(e -> e.getKey().getValue() + " " - + e.getValue() - .stream() - .map(r -> r.render(CSPSettingRequestCycleListener.this, cycle)) - .collect(Collectors.joining(" "))) - .collect(Collectors.joining("; ")); - } - } - - private Function<Integer, byte[]> randomSupplier; - - private Map<CSPHeaderMode, CSPConfiguration> configs = new HashMap<>(); - - public CSPSettingRequestCycleListener() - { - } - - public CSPSettingRequestCycleListener(Function<Integer, byte[]> randomSupplier) - { - this.randomSupplier = randomSupplier; - } - - public CSPConfiguration blocking() - { - return configs.computeIfAbsent(CSPHeaderMode.BLOCKING, CSPConfiguration::new); - } - - public CSPConfiguration reporting() - { - return configs.computeIfAbsent(CSPHeaderMode.REPORT_ONLY, CSPConfiguration::new); + + protected boolean mustProtectPageRequest(IPageClassRequestHandler handler) { + return true; } @Override public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler) { + if (!mustProtect(handler)) + return; + WebResponse webResponse = (WebResponse) cycle.getResponse(); - configs.values().stream().filter(CSPConfiguration::isSet).forEach(config -> { - String headerValue = config.renderHeaderValue(cycle); - webResponse.setHeader(config.getMode().getHeader(), headerValue); + configs.entrySet().stream().filter(entry -> entry.getValue().isSet()).forEach(entry -> { + CSPHeaderMode mode = entry.getKey(); + CSPHeaderConfiguration config = entry.getValue(); + String headerValue = config.renderHeaderValue(this, cycle); + webResponse.setHeader(mode.getHeader(), headerValue); if (config.isAddLegacyHeaders()) - webResponse.setHeader(config.getMode().getLegacyHeader(), headerValue); + webResponse.setHeader(mode.getLegacyHeader(), headerValue); }); } @@ -505,7 +130,7 @@ public class CSPSettingRequestCycleListener implements IRequestCycleListener String nonce = cycle.getMetaData(NONCE_KEY); if (nonce == null) { - nonce = Base64.getEncoder().encodeToString(randomSupplier.apply(12)); + nonce = application.getSecuritySettings().getRandomSupplier().getRandomBase64(12); cycle.setMetaData(NONCE_KEY, nonce); } return nonce; diff --git a/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java b/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java new file mode 100644 index 0000000..25886d6 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/csp/FixedCSPDirective.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket.csp; + +import org.apache.wicket.request.cycle.RequestCycle; +import org.apache.wicket.util.string.Strings; + +/** + * A simpel CSP directive that renders the string specified. + * + * @author papegaaij + */ +public class FixedCSPDirective implements CSPRenderable +{ + private String value; + + /** + * Creates a new {@code FixedCSPDirective} for the given value. + * + * @param value + * the value to render; + */ + public FixedCSPDirective(String value) + { + if (Strings.isEmpty(value)) + throw new IllegalArgumentException( + "CSP directive cannot have empty or null values"); + this.value = value; + } + + @Override + public String render(CSPSettingRequestCycleListener listener, RequestCycle cycle) + { + return value; + } +} \ No newline at end of file diff --git a/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java b/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java index 14ba223..28b292c 100644 --- a/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java +++ b/wicket-core/src/test/java/org/apache/wicket/csp/CSPSettingRequestCycleListenerTest.java @@ -16,16 +16,16 @@ */ package org.apache.wicket.csp; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.CHILD_SRC; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.DEFAULT_SRC; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.FRAME_SRC; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.REPORT_URI; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective.SANDBOX; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSandboxValue.ALLOW_FORMS; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSandboxValue.EMPTY; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue.NONE; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue.SELF; -import static org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue.WILDCARD; +import static org.apache.wicket.csp.CSPDirective.CHILD_SRC; +import static org.apache.wicket.csp.CSPDirective.DEFAULT_SRC; +import static org.apache.wicket.csp.CSPDirective.FRAME_SRC; +import static org.apache.wicket.csp.CSPDirective.REPORT_URI; +import static org.apache.wicket.csp.CSPDirective.SANDBOX; +import static org.apache.wicket.csp.CSPDirectiveSandboxValue.ALLOW_FORMS; +import static org.apache.wicket.csp.CSPDirectiveSandboxValue.EMPTY; +import static org.apache.wicket.csp.CSPDirectiveSrcValue.NONE; +import static org.apache.wicket.csp.CSPDirectiveSrcValue.SELF; +import static org.apache.wicket.csp.CSPDirectiveSrcValue.WILDCARD; import java.net.URI; import java.net.URISyntaxException; @@ -37,9 +37,6 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirective; -import org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSandboxValue; -import org.apache.wicket.csp.CSPSettingRequestCycleListener.CSPDirectiveSrcValue; import org.apache.wicket.mock.MockHomePage; import org.apache.wicket.util.tester.WicketTester; import org.junit.jupiter.api.Assertions; @@ -47,7 +44,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @SuppressWarnings("deprecation") -public class CSPSettingRequestCycleListenerTest +public class CSPSettingRequestCycleListenerTest extends WicketTester { private static String HEADER_CSP = "Content-Security-Policy"; @@ -64,7 +61,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testNullSrcInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, (String) null); }); @@ -73,7 +71,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testEmptySrcInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, ""); }); @@ -86,7 +85,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testInvalidSrcInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, "abc?^()-_\'xyz"); }); @@ -99,7 +99,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testMultipleSrcInputWithNoneIsRejected1() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, SELF, NONE); }); @@ -112,7 +113,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testMultipleSrcInputWithNoneIsRejected2() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, NONE, SELF); }); @@ -125,7 +127,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testMultipleSrcInputWithStarIsRejected1() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); cspListener.blocking().addDirective(DEFAULT_SRC, SELF); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, WILDCARD); @@ -139,7 +142,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testMultipleSrcInputWithStarIsRejected2() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); cspListener.blocking().addDirective(DEFAULT_SRC, WILDCARD); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, SELF); @@ -149,7 +153,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testWrongSrcInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(DEFAULT_SRC, ALLOW_FORMS); }); @@ -158,7 +163,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testWrongSandboxInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(SANDBOX, SELF); }); @@ -167,7 +173,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testNullSandboxInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(SANDBOX, (String) null); }); @@ -176,14 +183,16 @@ public class CSPSettingRequestCycleListenerTest @Test public void testEmptySandboxInputIsAccepted() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); cspListener.blocking().addDirective(SANDBOX, CSPDirectiveSandboxValue.EMPTY); } @Test public void testInvalidSandboxInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(SANDBOX, "abcxyz"); }); @@ -192,7 +201,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testMultipleSandboxInputWithEmptyStringIsRejected1() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); cspListener.blocking().addDirective(SANDBOX, ALLOW_FORMS); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(SANDBOX, EMPTY); @@ -202,7 +212,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testMultipleSandboxInputWithEmptyStringIsRejected2() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); cspListener.blocking().addDirective(SANDBOX, EMPTY); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(SANDBOX, ALLOW_FORMS); @@ -212,7 +223,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testNullReportUriInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(REPORT_URI, (String) null); }); @@ -221,7 +233,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testEmptyReportUriInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(REPORT_URI, ""); }); @@ -230,7 +243,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testInvalidReportUriInputIsRejected() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); Assertions.assertThrows(IllegalArgumentException.class, () -> { cspListener.blocking().addDirective(REPORT_URI, "abc?^()-_\'xyz"); }); @@ -239,12 +253,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testAllCSPSrcDefaultEnumsAreSetCorrectly() throws NoSuchAlgorithmException { - SecureRandom random = SecureRandom.getInstanceStrong(); - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(length -> { - byte[] ret = new byte[length]; - random.nextBytes(ret); - return ret; - }); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); final int cspDirectiveCount = CSPDirective.values().length; final int cspDirectiveSrcValueCount = CSPDirectiveSrcValue.values().length; @@ -273,7 +283,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testCSPReportUriDirectiveSetCorrectly() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); cspListener.blocking().addDirective(REPORT_URI, "http://report.example.com"); cspListener.reporting().addDirective(REPORT_URI, "/example-report-uri"); @@ -288,14 +299,15 @@ public class CSPSettingRequestCycleListenerTest @Test public void testCSPSandboxDirectiveSetCorrectly() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); final int cspSandboxDirectiveValueCount = CSPDirectiveSandboxValue.values().length; for (int i = 0; i < cspSandboxDirectiveValueCount; i++) { final CSPDirectiveSandboxValue cspDirectiveValue = CSPDirectiveSandboxValue.values()[i]; if (cspDirectiveValue.equals(CSPDirectiveSandboxValue.EMPTY)) continue; - + cspListener.blocking().addDirective(SANDBOX, cspDirectiveValue); cspListener.reporting().addDirective(SANDBOX, cspDirectiveValue); } @@ -316,7 +328,8 @@ public class CSPSettingRequestCycleListenerTest @Test public void testChildSrcDirectiveAlsoSetsFrameSrcDirective() { - CSPSettingRequestCycleListener cspListener = new CSPSettingRequestCycleListener(); + CSPSettingRequestCycleListener cspListener = + new CSPSettingRequestCycleListener(getApplication()); cspListener.blocking().addDirective(CHILD_SRC, SELF); cspListener.reporting().addDirective(CHILD_SRC, SELF); StringBuffer headerErrors = checkHeaders(cspListener); @@ -460,5 +473,4 @@ public class CSPSettingRequestCycleListenerTest return false; } - }
