Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wicket 6786: Add Fetch Metadata support #439

Merged
merged 9 commits into from Aug 7, 2020
Expand Up @@ -16,6 +16,11 @@
*/
package org.apache.wicket.protocol.http;

import static org.apache.wicket.protocol.http.ResourceIsolationPolicy.SEC_FETCH_DEST_HEADER;
import static org.apache.wicket.protocol.http.ResourceIsolationPolicy.SEC_FETCH_MODE_HEADER;
import static org.apache.wicket.protocol.http.ResourceIsolationPolicy.SEC_FETCH_SITE_HEADER;
import static org.apache.wicket.protocol.http.ResourceIsolationPolicy.VARY_HEADER;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
Expand All @@ -33,6 +38,8 @@
import org.apache.wicket.request.cycle.IRequestCycleListener;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;

import org.apache.wicket.request.http.WebResponse;
import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException;
import org.apache.wicket.util.lang.Checks;
import org.apache.wicket.util.string.Strings;
Expand Down Expand Up @@ -115,6 +122,11 @@ public class CsrfPreventionRequestCycleListener implements IRequestCycleListener
{
private static final Logger log = LoggerFactory
.getLogger(CsrfPreventionRequestCycleListener.class);
static final String VARY_HEADER_VALUE = String.format("%s, %s, %s,",
SEC_FETCH_DEST_HEADER,
SEC_FETCH_SITE_HEADER,
SEC_FETCH_MODE_HEADER
);

/**
* The action to perform when a missing or conflicting source URI is detected.
Expand Down Expand Up @@ -178,6 +190,15 @@ public String toString()
*/
private Collection<String> acceptedOrigins = new ArrayList<>();

private final ResourceIsolationPolicy resourceIsolationPolicy;

protected CsrfPreventionRequestCycleListener() {
this(new DefaultResourceIsolationPolicy());
}

protected CsrfPreventionRequestCycleListener(ResourceIsolationPolicy resourceIsolationPolicy) {
this.resourceIsolationPolicy = resourceIsolationPolicy;
}
/**
* Sets the action when no Origin header is present in the request. Default {@code ALLOW}.
*
Expand Down Expand Up @@ -312,6 +333,15 @@ protected boolean isChecked(IRequestHandler handler)
!(handler instanceof RenderPageRequestHandler);
}

/**
* Checks if Sec-Fetch-* headers are present
*/
protected boolean hasFetchMetadataHeaders(HttpServletRequest containerRequest)
{
String secFetchSiteValue = containerRequest.getHeader(SEC_FETCH_SITE_HEADER);
return !Strings.isEmpty(secFetchSiteValue);
}

/**
* Unwraps the handler if it is a {@code IRequestHandlerDelegate} down to the deepest nested
* handler.
Expand Down Expand Up @@ -350,8 +380,16 @@ public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler
// Check if the page should be CSRF protected
if (isChecked(targetedPage))
{
// if so check the Origin HTTP header
checkRequest(containerRequest, sourceUri, targetedPage);
// check sec-fetch-site header and call the fetch metadata check if present
if (hasFetchMetadataHeaders(containerRequest))
{
checkRequestFetchMetadata(containerRequest, sourceUri, targetedPage, cycle);
}
else
{
// if not check the Origin HTTP header
checkRequestOriginReferer(containerRequest, sourceUri, targetedPage);
}
}
else
{
Expand All @@ -372,6 +410,22 @@ public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler
}
}

@Override
public void onEndRequest(RequestCycle cycle)
{
// set vary headers to avoid caching responses processed by Fetch Metadata
// caching these responses may return 403 responses to legitimate requests
// or defeat the protection
if (cycle.getResponse() instanceof WebResponse)
{
WebResponse webResponse = (WebResponse)cycle.getResponse();
if (webResponse.isHeaderSupported())
{
webResponse.addHeader(VARY_HEADER, VARY_HEADER_VALUE);
}
}
}

/**
* Resolves the source URI from the request headers ({@code Origin} or {@code Referer}).
*
Expand Down Expand Up @@ -400,7 +454,8 @@ protected String getSourceUri(HttpServletRequest containerRequest)
* @param page
* the page that is the target of the request
*/
protected void checkRequest(HttpServletRequest request, String sourceUri, IRequestablePage page)
protected void checkRequestOriginReferer(HttpServletRequest request, String sourceUri,
IRequestablePage page)
salcho marked this conversation as resolved.
Show resolved Hide resolved
{
if (sourceUri == null || sourceUri.isEmpty())
{
Expand Down Expand Up @@ -451,6 +506,60 @@ protected void checkRequest(HttpServletRequest request, String sourceUri, IReque
}
}

/**
* Performs the check of the {@code Sec-Fetch-*} headers that are targeted at the {@code page}.
*
* @param request
* the current container request
* @param sourceUri
* the source URI
* @param page
* the page that is the target of the request
* @param cycle
* the current request cycle to set vary headers after white list check
*/
protected void checkRequestFetchMetadata(HttpServletRequest request, String sourceUri,
IRequestablePage page, RequestCycle cycle)
{
// check if sourceUri exists before checking for whitelisted hosts,
// if not set sourceUri to no origin for logging purposes
if (Strings.isEmpty(sourceUri))
{
sourceUri = "no origin";
}
else
{
// if the origin is a know and trusted origin, don't check any further but allow the
// request
if (isWhitelistedHost(sourceUri))
{
whitelistedHandler(request, sourceUri, page);
return;
}
}

if (resourceIsolationPolicy.isRequestAllowed(request))
{
matchingOrigin(request, sourceUri, page);
return;
}

log.debug("Request is not allowed by the resource isolation policy, {}",
conflictingOriginAction);
switch (conflictingOriginAction)
{
case ALLOW :
allowHandler(request, sourceUri, page);
break;
case SUPPRESS :
suppressHandler(request, sourceUri, page);
break;
case ABORT :
abortHandler(request, sourceUri, page);
break;
}
}

/**
* Checks whether the domain part of the {@code sourceUri} ({@code Origin} or {@code Referer}
* header) is whitelisted.
Expand Down
@@ -0,0 +1,60 @@
/*
* 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.protocol.http;

import javax.servlet.http.HttpServletRequest;

/**
* Default resource isolation policy used in {@link CsrfPreventionRequestCycleListener} that
* implements the {@link ResourceIsolationPolicy} interface. This default policy is based on
* <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>.
*
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
*
* @author Santiago Diaz - saldiaz@google.com
* @author Ecenaz Jen Ozmen - ecenazo@google.com
*/
public class DefaultResourceIsolationPolicy implements ResourceIsolationPolicy
{

@Override
public boolean isRequestAllowed(HttpServletRequest request)
{
String site = request.getHeader(SEC_FETCH_SITE_HEADER);

// Allow same-site and browser-initiated requests
if (SAME_ORIGIN.equals(site) || SAME_SITE.equals(site) || NONE.equals(site))
{
return true;
}

// Allow simple top-level navigations except <object> and <embed>
return isAllowedTopLevelNavigation(request);
}

private boolean isAllowedTopLevelNavigation(HttpServletRequest request)
{
String mode = request.getHeader(SEC_FETCH_MODE_HEADER);
String dest = request.getHeader(SEC_FETCH_DEST_HEADER);

boolean isSimpleTopLevelNavigation = MODE_NAVIGATE.equals(mode)
|| "GET".equals(request.getMethod());
boolean isNotObjectOrEmbedRequest = !DEST_EMBED.equals(dest) && !DEST_OBJECT.equals(dest);

return isSimpleTopLevelNavigation && isNotObjectOrEmbedRequest;
}
}
@@ -0,0 +1,54 @@
/*
* 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.protocol.http;

import javax.servlet.http.HttpServletRequest;

/**
* Interface for the resource isolation policies to be used for fetch metadata checks.
*
* Resource isolation policies are designed to protect against cross origin attacks and use the
* {@code sec-fetch-*} request headers to decide whether to accept or reject a request. Read more
* about <a href="https://web.dev/fetch-metadata/">Fetch Metadata.</a>
*
* See {@link DefaultResourceIsolationPolicy} for the default implementation used.
*
* @see <a href="https://web.dev/fetch-metadata/">https://web.dev/fetch-metadata/</a>
*
* @author Santiago Diaz - saldiaz@google.com
* @author Ecenaz Jen Ozmen - ecenazo@google.com
*/
@FunctionalInterface
public interface ResourceIsolationPolicy
{
String SEC_FETCH_SITE_HEADER = "sec-fetch-site";
String SEC_FETCH_MODE_HEADER = "sec-fetch-mode";
String SEC_FETCH_DEST_HEADER = "sec-fetch-dest";
String VARY_HEADER = "Vary";
String SAME_ORIGIN = "same-origin";
String SAME_SITE = "same-site";
String NONE = "none";
String MODE_NAVIGATE = "navigate";
String DEST_OBJECT = "object";
String DEST_EMBED = "embed";
String CROSS_SITE = "cross-site";
String CORS = "cors";
String DEST_SCRIPT = "script";
String DEST_IMAGE = "image";

boolean isRequestAllowed(HttpServletRequest request);
}