001package org.apache.archiva.redback.rest.services.interceptors; 002/* 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 */ 020 021 022import org.apache.archiva.redback.authentication.AuthenticationResult; 023import org.apache.archiva.redback.authentication.InvalidTokenException; 024import org.apache.archiva.redback.authentication.TokenData; 025import org.apache.archiva.redback.authentication.TokenManager; 026import org.apache.archiva.redback.authorization.RedbackAuthorization; 027import org.apache.archiva.redback.configuration.UserConfiguration; 028import org.apache.archiva.redback.configuration.UserConfigurationKeys; 029import org.apache.archiva.redback.integration.filter.authentication.basic.HttpBasicAuthentication; 030import org.apache.archiva.redback.users.User; 031import org.apache.commons.lang3.StringUtils; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034import org.springframework.stereotype.Service; 035 036import javax.annotation.PostConstruct; 037import javax.annotation.Priority; 038import javax.inject.Inject; 039import javax.inject.Named; 040import javax.servlet.http.HttpServletRequest; 041import javax.ws.rs.container.ContainerRequestContext; 042import javax.ws.rs.container.ContainerRequestFilter; 043import javax.ws.rs.container.ContainerResponseContext; 044import javax.ws.rs.container.ContainerResponseFilter; 045import javax.ws.rs.container.ResourceInfo; 046import javax.ws.rs.core.Context; 047import javax.ws.rs.core.Response; 048import javax.ws.rs.ext.Provider; 049import java.io.IOException; 050import java.net.MalformedURLException; 051import java.net.URL; 052import java.util.ArrayList; 053import java.util.List; 054 055/** 056 * Created by Martin Stockhammer on 19.01.17. 057 * <p> 058 * This interceptor tries to check if requests come from a valid origin and 059 * are not generated by another site on behalf of the real client. 060 * <p> 061 * We are using some of the techniques mentioned in 062 * https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet 063 * <p> 064 * Try to find Origin and Referer of the request. 065 * Match them to the target address, that may be either statically configured or is determined 066 * by the Host/X-Forwarded-For Header. 067 */ 068@Provider 069@Service( "requestValidationInterceptor#rest" ) 070@Priority( Priorities.PRECHECK ) 071public class RequestValidationInterceptor 072 extends AbstractInterceptor 073 implements ContainerRequestFilter, ContainerResponseFilter 074{ 075 076 077 private static final String X_FORWARDED_PROTO = "X-Forwarded-Proto"; 078 079 private static final String X_FORWARDED_HOST = "X-Forwarded-Host"; 080 081 private static final String X_XSRF_TOKEN = "X-XSRF-TOKEN"; 082 083 private static final String ORIGIN = "Origin"; 084 085 private static final String REFERER = "Referer"; 086 087 private static final int DEFAULT_HTTP = 80; 088 089 private static final int DEFAULT_HTTPS = 443; 090 091 private final Logger log = LoggerFactory.getLogger( getClass() ); 092 093 private boolean enabled = true; 094 095 private boolean checkToken = true; 096 097 private boolean useStaticUrl = false; 098 099 private boolean denyAbsentHeaders = true; 100 101 private List<URL> baseUrl = new ArrayList<URL>(); 102 103 private HttpServletRequest httpRequest = null; 104 105 @Inject 106 @Named( value = "httpAuthenticator#basic" ) 107 private HttpBasicAuthentication httpAuthenticator; 108 109 @Inject 110 @Named( value = "tokenManager#default" ) 111 TokenManager tokenManager; 112 113 @Context 114 private ResourceInfo resourceInfo; 115 116 private UserConfiguration config; 117 118 @Override 119 public void filter( ContainerRequestContext requestContext, ContainerResponseContext responseContext ) throws IOException 120 { 121 responseContext.getHeaders().add( 122 "Access-Control-Allow-Origin", "http://localhost:4200"); 123 responseContext.getHeaders().add( 124 "Access-Control-Allow-Credentials", "true"); 125 responseContext.getHeaders().add( 126 "Access-Control-Allow-Headers", 127 "origin, content-type, accept, authorization"); 128 responseContext.getHeaders().add( 129 "Access-Control-Allow-Methods", 130 "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH"); 131 } 132 133 private class HeaderValidationInfo 134 { 135 136 final static int UNKNOWN = -1; 137 138 final static int OK = 0; 139 140 final static int F_REFERER_HOST = 1; 141 142 final static int F_REFERER_PORT = 2; 143 144 final static int F_ORIGIN_HOST = 8; 145 146 final static int F_ORIGIN_PORT = 16; 147 148 final static int F_ORIGIN_PROTOCOL = 32; 149 150 boolean headerFound = false; 151 152 URL targetUrl; 153 154 URL originUrl; 155 156 URL refererUrl; 157 158 String targetHost; 159 160 String originHost; 161 162 String refererHost; 163 164 int targetPort; 165 166 int originPort; 167 168 int refererPort; 169 170 int status = UNKNOWN; 171 172 public HeaderValidationInfo( URL targetUrl ) 173 { 174 setTargetUrl( targetUrl ); 175 } 176 177 public URL getTargetUrl() 178 { 179 return targetUrl; 180 } 181 182 public void setTargetUrl( URL targetUrl ) 183 { 184 this.targetUrl = targetUrl; 185 this.targetHost = getHost( targetUrl ); 186 this.targetPort = getPort( targetUrl ); 187 } 188 189 public URL getOriginUrl() 190 { 191 return originUrl; 192 } 193 194 public void setOriginUrl( URL originUrl ) 195 { 196 this.originUrl = originUrl; 197 this.originHost = getHost( originUrl ); 198 this.originPort = getPort( originUrl ); 199 checkOrigin(); 200 this.headerFound = true; 201 } 202 203 public URL getRefererUrl() 204 { 205 return refererUrl; 206 } 207 208 public void setRefererUrl( URL refererUrl ) 209 { 210 this.refererUrl = refererUrl; 211 this.refererHost = getHost( refererUrl ); 212 this.refererPort = getPort( refererUrl ); 213 checkReferer(); 214 this.headerFound = true; 215 } 216 217 public String getTargetHost() 218 { 219 return targetHost; 220 } 221 222 public void setTargetHost( String targetHost ) 223 { 224 this.targetHost = targetHost; 225 } 226 227 public String getOriginHost() 228 { 229 return originHost; 230 } 231 232 public void setOriginHost( String originHost ) 233 { 234 this.originHost = originHost; 235 } 236 237 public String getRefererHost() 238 { 239 return refererHost; 240 } 241 242 public void setRefererHost( String refererHost ) 243 { 244 this.refererHost = refererHost; 245 } 246 247 public int getTargetPort() 248 { 249 return targetPort; 250 } 251 252 public void setTargetPort( int targetPort ) 253 { 254 this.targetPort = targetPort; 255 } 256 257 public int getOriginPort() 258 { 259 return originPort; 260 } 261 262 public void setOriginPort( int originPort ) 263 { 264 this.originPort = originPort; 265 } 266 267 public int getRefererPort() 268 { 269 return refererPort; 270 } 271 272 public void setRefererPort( int refererPort ) 273 { 274 this.refererPort = refererPort; 275 } 276 277 public void setStatus( int status ) 278 { 279 this.status |= status; 280 } 281 282 public int getStatus() 283 { 284 return this.status; 285 } 286 287 // Origin check for Protocol, Host, Port 288 public void checkOrigin() 289 { 290 if ( this.getStatus() == UNKNOWN ) 291 { 292 this.status = OK; 293 } 294 if ( !targetUrl.getProtocol().equals( originUrl.getProtocol() ) ) 295 { 296 setStatus( F_ORIGIN_PROTOCOL ); 297 } 298 if ( !targetHost.equals( originHost ) ) 299 { 300 setStatus( F_ORIGIN_HOST ); 301 } 302 if ( targetPort != originPort ) 303 { 304 setStatus( F_ORIGIN_PORT ); 305 } 306 } 307 308 // Referer check only for Host, Port 309 public void checkReferer() 310 { 311 if ( this.getStatus() == UNKNOWN ) 312 { 313 this.status = OK; 314 } 315 if ( !targetHost.equals( refererHost ) ) 316 { 317 setStatus( F_REFERER_HOST ); 318 } 319 if ( targetPort != refererPort ) 320 { 321 setStatus( F_REFERER_PORT ); 322 } 323 } 324 325 public boolean hasOriginError() 326 { 327 return ( status & ( F_ORIGIN_PROTOCOL | F_ORIGIN_HOST | F_ORIGIN_PORT ) ) > 0; 328 } 329 330 public boolean hasRefererError() 331 { 332 return ( status & ( F_REFERER_HOST | F_REFERER_PORT ) ) > 0; 333 } 334 335 @Override 336 public String toString() 337 { 338 return "Stat=" + status + ", target=" + targetUrl + ", origin=" + originUrl + ", referer=" + refererUrl; 339 } 340 } 341 342 @Inject 343 public RequestValidationInterceptor( @Named( value = "userConfiguration#default" ) UserConfiguration config ) 344 { 345 this.config = config; 346 } 347 348 @PostConstruct 349 public void init() 350 { 351 List<String> baseUrlList = config.getList( UserConfigurationKeys.REST_BASE_URL ); 352 if ( baseUrlList != null ) 353 { 354 for ( String baseUrlStr : baseUrlList ) 355 { 356 if ( !"".equals( baseUrlStr.trim() ) ) 357 { 358 try 359 { 360 baseUrl.add( new URL( baseUrlStr ) ); 361 useStaticUrl = true; 362 } 363 catch ( MalformedURLException ex ) 364 { 365 log.error( "Configured baseUrl (rest.baseUrl={}) is invalid. Message: {}", baseUrlStr, 366 ex.getMessage() ); 367 } 368 } 369 } 370 } 371 denyAbsentHeaders = config.getBoolean( UserConfigurationKeys.REST_CSRF_ABSENTORIGIN_DENY, true ); 372 enabled = config.getBoolean( UserConfigurationKeys.REST_CSRF_ENABLED, true ); 373 if ( !enabled ) 374 { 375 log.info( "CSRF Filter is disabled by configuration" ); 376 } 377 else 378 { 379 log.info( "CSRF Filter is enable" ); 380 } 381 checkToken = !config.getBoolean( UserConfigurationKeys.REST_CSRF_DISABLE_TOKEN_VALIDATION, false ); 382 if ( !checkToken ) 383 { 384 log.info( "CSRF Token validation is disabled by configuration" ); 385 } 386 else 387 { 388 log.info( "CSRF Token validation is enable" ); 389 } 390 } 391 392 @Override 393 public void filter( ContainerRequestContext containerRequestContext ) 394 throws IOException 395 { 396 397 if ( enabled ) 398 { 399 400 final String requestPath = containerRequestContext.getUriInfo( ).getPath( ); 401 if (ignoreAuth( requestPath )) { 402 return; 403 } 404 405 HttpServletRequest request = getRequest(); 406 List<URL> targetUrls = getTargetUrl( request ); 407 if ( targetUrls == null ) 408 { 409 log.error( "Could not verify target URL." ); 410 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 411 return; 412 } 413 List<HeaderValidationInfo> validationInfos = new ArrayList<HeaderValidationInfo>(); 414 boolean targetMatch = false; 415 boolean noHeader = true; 416 for ( URL targetUrl : targetUrls ) 417 { 418 log.trace( "Checking against target URL: {}", targetUrl ); 419 HeaderValidationInfo info = checkSourceRequestHeader( new HeaderValidationInfo( targetUrl ), request ); 420 // We need only one match 421 noHeader = noHeader && info.getStatus() == info.UNKNOWN; 422 if ( info.getStatus() == info.OK ) 423 { 424 targetMatch = true; 425 break; 426 } 427 else 428 { 429 validationInfos.add( info ); 430 } 431 } 432 if ( noHeader && denyAbsentHeaders ) 433 { 434 log.warn( "Request denied. No Origin or Referer header found and {}=true", 435 UserConfigurationKeys.REST_CSRF_ABSENTORIGIN_DENY ); 436 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 437 return; 438 } 439 if ( !targetMatch ) 440 { 441 log.warn( "HTTP Header check failed. Assuming CSRF attack." ); 442 for ( HeaderValidationInfo info : validationInfos ) 443 { 444 if ( info.hasOriginError() ) 445 { 446 log.warn( 447 "Origin Header does not match: originUrl={}, targetUrl={}. Matches: Host={}, Port={}, Protocol={}", 448 info.originUrl, info.targetUrl, ( info.getStatus() & info.F_ORIGIN_HOST ) == 0, 449 ( info.getStatus() & info.F_ORIGIN_PORT ) == 0, 450 ( info.getStatus() & info.F_ORIGIN_PROTOCOL ) == 0 ); 451 } 452 if ( info.hasRefererError() ) 453 { 454 log.warn( 455 "Referer Header does not match: refererUrl={}, targetUrl={}. Matches: Host={}, Port={}", 456 info.refererUrl, info.targetUrl, ( info.getStatus() & info.F_REFERER_HOST ) == 0, 457 ( info.getStatus() & info.F_REFERER_PORT ) == 0 ); 458 } 459 } 460 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 461 return; 462 } 463 if ( checkToken ) 464 { 465 checkValidationToken( containerRequestContext, request ); 466 } 467 } 468 } 469 470 /** 471 * Checks the request for a validation token header. It takes the encrypted token, decrypts it 472 * and compares the user information from the token to the logged in user. 473 * 474 * @param containerRequestContext 475 * @param request 476 */ 477 private void checkValidationToken( ContainerRequestContext containerRequestContext, HttpServletRequest request ) 478 { 479 RedbackAuthorization redbackAuthorization = getRedbackAuthorization( resourceInfo ); 480 // We check only services that are restricted 481 if ( !redbackAuthorization.noRestriction() ) 482 { 483 String tokenString = request.getHeader( X_XSRF_TOKEN ); 484 if ( tokenString == null || tokenString.length() == 0 ) 485 { 486 log.warn( "No validation token header found: {}", X_XSRF_TOKEN ); 487 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 488 return; 489 } 490 491 try 492 { 493 TokenData td = tokenManager.decryptToken( tokenString ); 494 AuthenticationResult auth = getAuthenticationResult( containerRequestContext, httpAuthenticator, request ); 495 if ( auth == null ) 496 { 497 log.error( "Not authentication data found" ); 498 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 499 return; 500 } 501 User loggedIn = auth.getUser(); 502 if ( loggedIn == null ) 503 { 504 log.error( "User not logged in" ); 505 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 506 return; 507 } 508 String username = loggedIn.getUsername(); 509 if ( !td.isValid() || !td.getUser().equals( username ) ) 510 { 511 log.error( "Invalid data in validation token header {} for user {}: isValid={}, username={}", 512 X_XSRF_TOKEN, username, td.isValid(), td.getUser() ); 513 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 514 } 515 } 516 catch ( InvalidTokenException e ) 517 { 518 log.error( "Token validation failed {}", e.getMessage() ); 519 containerRequestContext.abortWith( Response.status( Response.Status.FORBIDDEN ).build() ); 520 } 521 } 522 log.debug( "Token validated" ); 523 } 524 525 private HttpServletRequest getRequest() 526 { 527 if ( httpRequest != null ) 528 { 529 return httpRequest; 530 } 531 else 532 { 533 return getHttpServletRequest( ); 534 } 535 } 536 537 private List<URL> getTargetUrl( HttpServletRequest request ) 538 { 539 if ( useStaticUrl ) 540 { 541 return baseUrl; 542 } 543 else 544 { 545 List<URL> urls = new ArrayList<URL>(); 546 URL requestUrl; 547 try 548 { 549 requestUrl = new URL( request.getRequestURL().toString() ); 550 urls.add( requestUrl ); 551 } 552 catch ( MalformedURLException ex ) 553 { 554 log.error( "Bad Request URL {}, Message: {}", request.getRequestURL(), ex.getMessage() ); 555 return null; 556 } 557 String xforwarded = request.getHeader( X_FORWARDED_HOST ); 558 String xforwardedProto = request.getHeader( X_FORWARDED_PROTO ); 559 if ( xforwardedProto == null ) 560 { 561 xforwardedProto = requestUrl.getProtocol(); 562 } 563 564 if ( xforwarded != null && !StringUtils.isEmpty( xforwarded ) ) 565 { 566 // X-Forwarded-Host header may contain multiple hosts if there is 567 // more than one proxy between the client and the server 568 String[] forwardedList = xforwarded.split( "\\s*,\\s*" ); 569 for ( String hostname : forwardedList ) 570 { 571 try 572 { 573 urls.add( new URL( xforwardedProto + "://" + hostname ) ); 574 } 575 catch ( MalformedURLException ex ) 576 { 577 log.warn( "X-Forwarded-Host Header is malformed: {}", ex.getMessage() ); 578 } 579 } 580 } 581 return urls; 582 } 583 } 584 585 private int getPort( final URL url ) 586 { 587 return url.getPort() > 0 588 ? url.getPort() 589 : ( "https".equals( url.getProtocol() ) ? DEFAULT_HTTPS : DEFAULT_HTTP ); 590 } 591 592 private String getHost( final URL url ) 593 { 594 return url.getHost().trim().toLowerCase(); 595 } 596 597 /** 598 * Checks the validation headers. First the Origin header is checked, if this fails 599 * or is absent, the referer header is checked. 600 * 601 * @param info The info object that must be populated with the targetURL 602 * @param request The HTTP request object 603 * @return A info object with updated status information 604 */ 605 private HeaderValidationInfo checkSourceRequestHeader( final HeaderValidationInfo info, 606 final HttpServletRequest request ) 607 { 608 String origin = request.getHeader( ORIGIN ); 609 if ( origin != null ) 610 { 611 try 612 { 613 info.setOriginUrl( new URL( origin ) ); 614 } 615 catch ( MalformedURLException e ) 616 { 617 log.warn( "Bad origin header found: {}", origin ); 618 } 619 } 620 // Check referer if Origin header dos not match or is not available 621 if ( info.getStatus() != info.OK ) 622 { 623 String referer = request.getHeader( REFERER ); 624 if ( referer != null ) 625 { 626 try 627 { 628 info.setRefererUrl( new URL( referer ) ); 629 } 630 catch ( MalformedURLException ex ) 631 { 632 log.warn( "Bad URL in Referer HTTP-Header: {}, Message: {}", referer, ex.getMessage() ); 633 } 634 } 635 } 636 return info; 637 } 638 639 public void setHttpRequest( HttpServletRequest request ) 640 { 641 this.httpRequest = request; 642 } 643 644}