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 @@ -110,11 +110,15 @@
* <li>{@link #onSuppressed(HttpServletRequest, String, IRequestablePage)} when an origin was in
* conflict and the request should be suppressed</li>
* </ul>
*
* @see FetchMetadataRequestCycleListener
* @deprecated
*/
public class CsrfPreventionRequestCycleListener implements IRequestCycleListener
@Deprecated(since = "XXX")
salcho marked this conversation as resolved.
Show resolved Hide resolved
public class CsrfPreventionRequestCycleListener implements IRequestCycleListener, ResourceIsolationPolicy
{
private static final Logger log = LoggerFactory
.getLogger(CsrfPreventionRequestCycleListener.class);
.getLogger(CsrfPreventionRequestCycleListener.class);

/**
* The action to perform when a missing or conflicting source URI is detected.
Expand Down Expand Up @@ -268,7 +272,7 @@ public void onBeginRequest(RequestCycle cycle)
if (log.isDebugEnabled())
{
HttpServletRequest containerRequest = (HttpServletRequest)cycle.getRequest()
.getContainerRequest();
.getContainerRequest();
log.debug("Request Source URI: {}", getSourceUri(containerRequest));
}
}
Expand Down Expand Up @@ -309,7 +313,7 @@ protected boolean isChecked(IRequestablePage targetedPage)
protected boolean isChecked(IRequestHandler handler)
{
return handler instanceof IPageRequestHandler &&
!(handler instanceof RenderPageRequestHandler);
!(handler instanceof RenderPageRequestHandler);
}

/**
Expand Down Expand Up @@ -344,7 +348,7 @@ public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler
IPageRequestHandler prh = (IPageRequestHandler)handler;
IRequestablePage targetedPage = prh.getPage();
HttpServletRequest containerRequest = (HttpServletRequest)cycle.getRequest()
.getContainerRequest();
.getContainerRequest();
String sourceUri = getSourceUri(containerRequest);

// Check if the page should be CSRF protected
Expand All @@ -367,8 +371,8 @@ public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler
{
if (log.isTraceEnabled())
log.trace(
"Resolved handler {} doesn't target an action on a page, no CSRF check performed",
handler.getClass().getName());
"Resolved handler {} doesn't target an action on a page, no CSRF check performed",
handler.getClass().getName());
}
}

Expand All @@ -389,6 +393,38 @@ protected String getSourceUri(HttpServletRequest containerRequest)
return normalizeUri(sourceUri);
}

/**
* This origin-based listener can be used in combination with the {@link FetchMetadataRequestCycleListener}
* to add support for legacy browsers that don't send Sec-Fetch-* headers yet.
* @return whether the request is allowed based on its origin
*/
@Override
public boolean isRequestAllowed(HttpServletRequest request, IRequestablePage targetPage) {
String sourceUri = getSourceUri(request);

if (sourceUri == null || sourceUri.isEmpty())
{
log.debug("Source URI not present in request, {}", noOriginAction);
return true;
}
sourceUri = sourceUri.toLowerCase(Locale.ROOT);

// if the origin is a know and trusted origin, don't check any further but allow the request
if (isWhitelistedHost(sourceUri))
{
return true;
}

// check if the origin HTTP header matches the request URI
if (!isLocalOrigin(request, sourceUri))
{
log.debug("Source URI conflicts with request origin, {}", conflictingOriginAction);
return false;
}

return true;
}

/**
* Performs the check of the {@code Origin} or {@code Referer} header that is targeted at the
* {@code page}.
Expand Down Expand Up @@ -469,18 +505,18 @@ protected boolean isWhitelistedHost(final String sourceUri)
for (String whitelistedOrigin : acceptedOrigins)
{
if (sourceHost.equalsIgnoreCase(whitelistedOrigin) ||
sourceHost.endsWith("." + whitelistedOrigin))
sourceHost.endsWith("." + whitelistedOrigin))
{
log.trace("Origin {} matched whitelisted origin {}, request accepted",
sourceUri, whitelistedOrigin);
sourceUri, whitelistedOrigin);
return true;
}
}
}
catch (URISyntaxException e)
{
log.debug("Origin: {} not parseable as an URI. Whitelisted-origin check skipped.",
sourceUri);
sourceUri);
}

return false;
Expand Down Expand Up @@ -624,13 +660,13 @@ protected final String getTargetUriFromRequest(HttpServletRequest request)
* the page that is targeted with this request
*/
protected void whitelistedHandler(HttpServletRequest request, String origin,
IRequestablePage page)
IRequestablePage page)
{
onWhitelisted(request, origin, page);
if (log.isDebugEnabled())
{
log.debug("CSRF Origin {} was whitelisted, allowed for page {}", origin,
page.getClass().getName());
page.getClass().getName());
}
}

Expand Down Expand Up @@ -661,13 +697,13 @@ protected void onWhitelisted(HttpServletRequest request, String origin, IRequest
* the page that is targeted with this request
*/
protected void matchingOrigin(HttpServletRequest request, String origin,
IRequestablePage page)
IRequestablePage page)
{
onMatchingOrigin(request, origin, page);
if (log.isDebugEnabled())
{
log.debug("CSRF Origin {} matched requested resource, allowed for page {}", origin,
page.getClass().getName());
page.getClass().getName());
}
}

Expand All @@ -683,7 +719,7 @@ protected void matchingOrigin(HttpServletRequest request, String origin,
* the page that is targeted with this request
*/
protected void onMatchingOrigin(HttpServletRequest request, String origin,
IRequestablePage page)
IRequestablePage page)
{
}

Expand All @@ -700,11 +736,11 @@ protected void onMatchingOrigin(HttpServletRequest request, String origin,
* the page that is targeted with this request
*/
protected void allowHandler(HttpServletRequest request, String origin,
IRequestablePage page)
IRequestablePage page)
{
onAllowed(request, origin, page);
log.info("Possible CSRF attack, request URL: {}, Origin: {}, action: allowed",
request.getRequestURL(), origin);
request.getRequestURL(), origin);
}

/**
Expand Down Expand Up @@ -736,11 +772,11 @@ protected void onAllowed(HttpServletRequest request, String origin, IRequestable
* the page that is targeted with this request
*/
protected void suppressHandler(HttpServletRequest request, String origin,
IRequestablePage page)
IRequestablePage page)
{
onSuppressed(request, origin, page);
log.info("Possible CSRF attack, request URL: {}, Origin: {}, action: suppressed",
request.getRequestURL(), origin);
request.getRequestURL(), origin);
throw new RestartResponseException(page);
}

Expand Down Expand Up @@ -773,12 +809,12 @@ protected void onSuppressed(HttpServletRequest request, String origin, IRequesta
* the page that is targeted with this request
*/
protected void abortHandler(HttpServletRequest request, String origin,
IRequestablePage page)
IRequestablePage page)
{
onAborted(request, origin, page);
log.info(
"Possible CSRF attack, request URL: {}, Origin: {}, action: aborted with error {} {}",
request.getRequestURL(), origin, errorCode, errorMessage);
"Possible CSRF attack, request URL: {}, Origin: {}, action: aborted with error {} {}",
request.getRequestURL(), origin, errorCode, errorMessage);
throw new AbortWithHttpErrorCodeException(errorCode, errorMessage);
}

Expand Down
@@ -0,0 +1,77 @@
/*
* 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;
import org.apache.wicket.request.component.IRequestablePage;
import org.apache.wicket.util.string.Strings;

/**
* Default resource isolation policy used in {@link FetchMetadataRequestCycleListener} 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,
IRequestablePage targetPage)
{
// request made by a legacy browser with no support for Fetch Metadata
if (!hasFetchMetadataHeaders(request)) {
return true;
}

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;
}

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