This repository has been archived by the owner on Apr 25, 2024. It is now read-only.
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implementing Header verification for REST API calls
Implementing header verification techniques mentioned in: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#Verifying_Same_Origin_with_Standard_Headers
- Loading branch information
Showing
8 changed files
with
670 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
213 changes: 213 additions & 0 deletions
213
...a/org/apache/archiva/redback/rest/services/interceptors/RequestValidationInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
package org.apache.archiva.redback.rest.services.interceptors; | ||
/* | ||
* 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. | ||
*/ | ||
|
||
|
||
import org.apache.archiva.redback.configuration.UserConfiguration; | ||
import org.apache.cxf.jaxrs.utils.JAXRSUtils; | ||
import org.apache.cxf.message.Message; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
import org.springframework.stereotype.Service; | ||
|
||
import javax.annotation.PostConstruct; | ||
import javax.inject.Inject; | ||
import javax.inject.Named; | ||
import javax.servlet.http.HttpServletRequest; | ||
import javax.ws.rs.container.ContainerRequestContext; | ||
import javax.ws.rs.container.ContainerRequestFilter; | ||
import javax.ws.rs.core.Response; | ||
import javax.ws.rs.ext.Provider; | ||
import java.io.IOException; | ||
import java.net.MalformedURLException; | ||
import java.net.URL; | ||
|
||
/** | ||
* Created by Martin Stockhammer on 19.01.17. | ||
* | ||
* This interceptor tries to check if requests come from a valid origin and | ||
* are not generated by another site on behalf of the real client. | ||
* | ||
* We are using some of the techniques mentioned in | ||
* https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet | ||
* | ||
* Try to find Origin and Referer of the request. | ||
* Match them to the target address, that may be either statically configured or is determined | ||
* by the Host/X-Forwarded-For Header. | ||
* | ||
* | ||
*/ | ||
@Provider | ||
@Service( "requestValidationInterceptor#rest" ) | ||
public class RequestValidationInterceptor extends AbstractInterceptor implements ContainerRequestFilter { | ||
|
||
|
||
private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; | ||
private static final String X_FORWARDED_HOST = "X-Forwarded-Host"; | ||
private static final String ORIGIN = "Origin"; | ||
private static final String REFERER = "Referer"; | ||
private static final String CFG_REST_BASE_URL = "rest.baseUrl"; | ||
private static final String CFG_REST_CSRF_ABSENTORIGIN_DENY = "rest.csrffilter.absentorigin.deny"; | ||
private static final String CFG_REST_CSRF_ENABLED = "rest.csrffilter.enabled"; | ||
|
||
private final Logger log = LoggerFactory.getLogger( getClass() ); | ||
|
||
private boolean enabled = true; | ||
private boolean useStaticUrl = false; | ||
private boolean denyAbsentHeaders = true; | ||
private URL baseUrl; | ||
private HttpServletRequest httpRequest = null; | ||
|
||
private UserConfiguration config; | ||
|
||
@Inject | ||
public RequestValidationInterceptor(@Named( value = "userConfiguration#default" ) | ||
UserConfiguration config) { | ||
this.config = config; | ||
} | ||
|
||
@PostConstruct | ||
public void init() { | ||
String baseUrlStr = config.getString(CFG_REST_BASE_URL, ""); | ||
if (!"".equals(baseUrlStr.trim())) { | ||
try { | ||
baseUrl = new URL(baseUrlStr); | ||
useStaticUrl = true; | ||
} catch (MalformedURLException ex) { | ||
log.error("Configured baseUrl (rest.baseUrl={}) is invalid. Message: {}", baseUrlStr, ex.getMessage()); | ||
} | ||
} else { | ||
useStaticUrl = false; | ||
} | ||
denyAbsentHeaders = config.getBoolean(CFG_REST_CSRF_ABSENTORIGIN_DENY,true); | ||
enabled = config.getBoolean(CFG_REST_CSRF_ENABLED,true); | ||
if (!enabled) { | ||
log.info("CSRF Filter is disabled by configuration"); | ||
} | ||
} | ||
|
||
@Override | ||
public void filter(ContainerRequestContext containerRequestContext) throws IOException { | ||
if (enabled) { | ||
HttpServletRequest request = getRequest(); | ||
URL targetUrl = getTargetUrl(request); | ||
if (targetUrl == null) { | ||
log.error("Could not verify target URL."); | ||
containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); | ||
} | ||
if (!checkSourceRequestHeader(targetUrl, request)) { | ||
log.warn("HTTP Header check failed. Assuming CSRF attack."); | ||
containerRequestContext.abortWith(Response.status(Response.Status.FORBIDDEN).build()); | ||
} | ||
} | ||
} | ||
|
||
private HttpServletRequest getRequest() { | ||
if (httpRequest!=null) { | ||
return httpRequest; | ||
} else { | ||
Message message = JAXRSUtils.getCurrentMessage(); | ||
return getHttpServletRequest(message); | ||
} | ||
} | ||
|
||
private URL getTargetUrl(HttpServletRequest request) { | ||
if (useStaticUrl) { | ||
return baseUrl; | ||
} else { | ||
URL requestUrl; | ||
try { | ||
requestUrl = new URL(request.getRequestURL().toString()); | ||
} catch (MalformedURLException ex) { | ||
log.error("Bad Request URL {}, Message: {}", request.getRequestURL(), ex.getMessage()); | ||
return null; | ||
} | ||
String xforwarded = request.getHeader(X_FORWARDED_HOST); | ||
String xforwardedProto = request.getHeader(X_FORWARDED_PROTO); | ||
if (xforwardedProto==null) { | ||
xforwardedProto=requestUrl.getProtocol(); | ||
} | ||
if (xforwarded!=null) { | ||
try { | ||
return new URL(xforwardedProto+"://"+xforwarded); | ||
} catch (MalformedURLException ex) { | ||
log.warn("X-Forwarded-Host Header is malformed: {}", ex.getMessage()); | ||
} | ||
} | ||
return requestUrl; | ||
} | ||
} | ||
|
||
private int getPort(final URL url) { | ||
return url.getPort() > 0 ? url.getPort() : url.getDefaultPort(); | ||
} | ||
|
||
private boolean checkSourceRequestHeader(final URL targetUrl, final HttpServletRequest request) { | ||
boolean headerFound=false; | ||
String origin = request.getHeader(ORIGIN); | ||
if (origin!=null) { | ||
try { | ||
URL originUrl = new URL(origin); | ||
headerFound=true; | ||
log.debug("Origin Header URL found: {}", originUrl); | ||
if (!targetUrl.getProtocol().equals(originUrl.getProtocol())) { | ||
log.warn("Origin Header Protocol does not match originUrl={}, targetUrl={}", originUrl, targetUrl); | ||
return false; | ||
} | ||
if (!targetUrl.getHost().equals(originUrl.getHost())) { | ||
log.warn("Origin Header Host does not match originUrl={}, targetUrl={}",originUrl,targetUrl); | ||
return false; | ||
} | ||
int originPort = getPort(originUrl); | ||
int targetPort = getPort(targetUrl); | ||
if (targetPort != originPort) { | ||
log.warn("Origin Header Port does not match originUrl={}, targetUrl={}",originUrl,targetUrl); | ||
return false; | ||
} | ||
} catch (MalformedURLException ex) { | ||
log.warn("Bad URL in Origin HTTP-Header: {}. Message: {}",origin, ex.getMessage()); | ||
return false; | ||
} | ||
} | ||
String referer = request.getHeader(REFERER); | ||
if (referer!=null) { | ||
try { | ||
URL refererUrl = new URL(referer); | ||
headerFound=true; | ||
log.debug("Referer Header URL found: {}",refererUrl); | ||
if (!targetUrl.getHost().equals(refererUrl.getHost())) { | ||
log.warn("Referer Header Host does not match refererUrl={}, targetUrl={}",refererUrl,targetUrl); | ||
return false; | ||
} | ||
} catch (MalformedURLException ex) { | ||
log.warn("Bad URL in Referer HTTP-Header: {}, Message: {}", referer, ex.getMessage()); | ||
return false; | ||
} | ||
} | ||
if (!headerFound && denyAbsentHeaders) { | ||
log.warn("Neither Origin nor Referer header found. Request is denied."); | ||
return false; | ||
} | ||
return true; | ||
} | ||
|
||
public void setHttpRequest(HttpServletRequest request) { | ||
this.httpRequest = request; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.