001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.protocol.http; 018 019import java.util.Locale; 020import javax.servlet.http.HttpServletRequest; 021import org.apache.wicket.RestartResponseException; 022import org.apache.wicket.core.request.handler.IPageRequestHandler; 023import org.apache.wicket.core.request.handler.RenderPageRequestHandler; 024import org.apache.wicket.request.IRequestHandler; 025import org.apache.wicket.request.IRequestHandlerDelegate; 026import org.apache.wicket.request.component.IRequestablePage; 027import org.apache.wicket.request.cycle.IRequestCycleListener; 028import org.apache.wicket.request.cycle.RequestCycle; 029import org.apache.wicket.request.http.WebRequest; 030import org.apache.wicket.request.http.flow.AbortWithHttpErrorCodeException; 031import org.apache.wicket.util.string.Strings; 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035/** 036 * Prevents CSRF attacks on Wicket components by checking the {@code Origin} and {@code Referer} 037 * HTTP headers for cross domain requests. By default only checks requests that try to perform an 038 * action on a component, such as a form submit, or link click. 039 * <p> 040 * Installation 041 * <p> 042 * You can enable this CSRF prevention filter by adding it to the request cycle listeners in your 043 * {@link WebApplication#init() application's init method}: 044 * 045 * <pre> 046 * @Override 047 * protected void init() 048 * { 049 * // ... 050 * getRequestCycleListeners().add(new CsrfPreventionRequestCycleListener()); 051 * // ... 052 * } 053 * </pre> 054 * <p> 055 * Configuration 056 * <p> 057 * When the {@code Origin} or {@code Referer} HTTP header is present but doesn't match the requested 058 * URL this listener will by default throw a HTTP error ( {@code 400 BAD REQUEST}) and abort the 059 * request. You can {@link #setConflictingOriginAction(CsrfAction) configure} this specific action. 060 * <p> 061 * A missing {@code Origin} and {@code Referer} HTTP header is handled as if it were a bad request 062 * and rejected. You can {@link #setNoOriginAction(CsrfAction) configure the specific action} to a 063 * different value, suppressing or allowing the request when the HTTP headers are missing. 064 * <p> 065 * When the {@code Origin} HTTP header is present and has the value {@code null} it is considered to 066 * be from a "privacy-sensitive" context and will trigger the no origin action. You can customize 067 * what happens in those actions by overriding the respective {@code onXXXX} methods. 068 * <p> 069 * When you want to accept certain cross domain request from a range of hosts, you can 070 * {@link #addAcceptedOrigin(String) whitelist those domains}. 071 * <p> 072 * You can {@link #isEnabled() enable or disable} this listener by overriding {@link #isEnabled()}. 073 * <p> 074 * You can {@link #isChecked(IRequestablePage) customize} whether a particular page should be 075 * checked for CSRF requests. For example you can skip checking pages that have a 076 * {@code @NoCsrfCheck} annotation, or only those pages that extend your base secure page class. For 077 * example: 078 * 079 * <pre> 080 * @Override 081 * protected boolean isChecked(IRequestablePage requestedPage) 082 * { 083 * return requestedPage instanceof SecurePage; 084 * } 085 * </pre> 086 * <p> 087 * You can also tweak the request handlers that are checked. The CSRF prevention request cycle 088 * listener checks only action handlers, not render handlers. Override 089 * {@link #isChecked(IRequestHandler)} to customize this behavior. 090 * </p> 091 * <p> 092 * You can customize the default actions that are performed by overriding the event handlers for 093 * them: 094 * <ul> 095 * <li>{@link #onWhitelisted(HttpServletRequest, String, IRequestablePage)} when an origin was 096 * whitelisted</li> 097 * <li>{@link #onMatchingOrigin(HttpServletRequest, String, IRequestablePage)} when an origin was 098 * matching</li> 099 * <li>{@link #onAborted(HttpServletRequest, String, IRequestablePage)} when an origin was in 100 * conflict and the request should be aborted</li> 101 * <li>{@link #onAllowed(HttpServletRequest, String, IRequestablePage)} when an origin was in 102 * conflict and the request should be allowed</li> 103 * <li>{@link #onSuppressed(HttpServletRequest, String, IRequestablePage)} when an origin was in 104 * conflict and the request should be suppressed</li> 105 * </ul> 106 * 107 * @deprecated Use {@link FetchMetadataResourceIsolationPolicy} instead 108 */ 109@Deprecated(since = "9.1.0") 110public class CsrfPreventionRequestCycleListener extends OriginResourceIsolationPolicy implements IRequestCycleListener 111{ 112 private static final Logger log = LoggerFactory 113 .getLogger(CsrfPreventionRequestCycleListener.class); 114 115 /** 116 * The action to perform when a missing or conflicting source URI is detected. 117 */ 118 public enum CsrfAction { 119 /** Aborts the request and throws an exception when a CSRF request is detected. */ 120 ABORT { 121 @Override 122 public String toString() 123 { 124 return "aborted"; 125 } 126 }, 127 128 /** 129 * Ignores the action of a CSRF request, and just renders the page it was targeted against. 130 */ 131 SUPPRESS { 132 @Override 133 public String toString() 134 { 135 return "suppressed"; 136 } 137 }, 138 139 /** Detects a CSRF request, logs it and allows the request to continue. */ 140 ALLOW { 141 @Override 142 public String toString() 143 { 144 return "allowed"; 145 } 146 }, 147 } 148 149 /** 150 * Action to perform when no Origin header is present in the request. 151 */ 152 private CsrfAction noOriginAction = CsrfAction.ABORT; 153 154 /** 155 * Action to perform when a conflicting Origin header is found. 156 */ 157 private CsrfAction conflictingOriginAction = CsrfAction.ABORT; 158 159 /** 160 * The error code to report when the action to take for a CSRF request is 161 * {@link CsrfAction#ABORT}. Default {@code 400 BAD REQUEST}. 162 */ 163 private int errorCode = javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; 164 165 /** 166 * The error message to report when the action to take for a CSRF request is {@code ERROR}. 167 * Default {@code "Origin does not correspond to request"}. 168 */ 169 private String errorMessage = "Origin does not correspond to request"; 170 171 /** 172 * TODO remove in Wicket 10 173 */ 174 @Override 175 public CsrfPreventionRequestCycleListener addAcceptedOrigin(String acceptedOrigin) 176 { 177 super.addAcceptedOrigin(acceptedOrigin); 178 179 return this; 180 } 181 182 /** 183 * Sets the action when no Origin header is present in the request. Default {@code ALLOW}. 184 * 185 * @param action 186 * the alternate action 187 * 188 * @return this (for chaining) 189 */ 190 public CsrfPreventionRequestCycleListener setNoOriginAction(CsrfAction action) 191 { 192 this.noOriginAction = action; 193 return this; 194 } 195 196 /** 197 * Sets the action when a conflicting Origin header is detected. Default is {@code ERROR}. 198 * 199 * @param action 200 * the alternate action 201 * 202 * @return this 203 */ 204 public CsrfPreventionRequestCycleListener setConflictingOriginAction(CsrfAction action) 205 { 206 this.conflictingOriginAction = action; 207 return this; 208 } 209 210 /** 211 * Modifies the HTTP error code in the exception when a conflicting Origin header is detected. 212 * 213 * @param errorCode 214 * the alternate HTTP error code, default {@code 400 BAD REQUEST} 215 * 216 * @return this 217 */ 218 public CsrfPreventionRequestCycleListener setErrorCode(int errorCode) 219 { 220 this.errorCode = errorCode; 221 return this; 222 } 223 224 /** 225 * Modifies the HTTP message in the exception when a conflicting Origin header is detected. 226 * 227 * @param errorMessage 228 * the alternate message 229 * 230 * @return this 231 */ 232 public CsrfPreventionRequestCycleListener setErrorMessage(String errorMessage) 233 { 234 this.errorMessage = errorMessage; 235 return this; 236 } 237 238 @Override 239 public void onBeginRequest(RequestCycle cycle) 240 { 241 if (log.isDebugEnabled()) 242 { 243 HttpServletRequest containerRequest = (HttpServletRequest)cycle.getRequest() 244 .getContainerRequest(); 245 log.debug("Request Source URI: {}", getSourceUri(containerRequest)); 246 } 247 } 248 249 /** 250 * Dynamic override for enabling/disabling the CSRF detection. Might be handy for specific 251 * tenants in a multi-tenant application. When false, the CSRF detection is not performed for 252 * the running request. Default {@code true} 253 * 254 * @return {@code true} when the CSRF checks need to be performed. 255 */ 256 protected boolean isEnabled() 257 { 258 return true; 259 } 260 261 /** 262 * Override to limit whether the request to the specific page should be checked for a possible 263 * CSRF attack. 264 * 265 * @param targetedPage 266 * the page that is the target for the action 267 * @return {@code true} when the request to the page should be checked for CSRF issues. 268 */ 269 protected boolean isChecked(IRequestablePage targetedPage) 270 { 271 return true; 272 } 273 274 /** 275 * Override to change the request handler types that are checked. Currently only action handlers 276 * (form submits, link clicks, AJAX events) are checked for a matching Origin HTTP header. 277 * 278 * @param handler 279 * the handler that is currently processing 280 * @return true when the Origin HTTP header should be checked for this {@code handler} 281 */ 282 protected boolean isChecked(IRequestHandler handler) 283 { 284 return handler instanceof IPageRequestHandler && 285 !(handler instanceof RenderPageRequestHandler); 286 } 287 288 /** 289 * Unwraps the handler if it is a {@code IRequestHandlerDelegate} down to the deepest nested 290 * handler. 291 * 292 * @param handler 293 * The handler to unwrap 294 * @return the deepest handler that does not implement {@code IRequestHandlerDelegate} 295 */ 296 protected IRequestHandler unwrap(IRequestHandler handler) 297 { 298 while (handler instanceof IRequestHandlerDelegate) 299 handler = ((IRequestHandlerDelegate)handler).getDelegateHandler(); 300 return handler; 301 } 302 303 @Override 304 public void onRequestHandlerResolved(RequestCycle cycle, IRequestHandler handler) 305 { 306 if (!isEnabled()) 307 { 308 log.trace("CSRF listener is disabled, no checks performed"); 309 return; 310 } 311 312 handler = unwrap(handler); 313 314 // check if the request is targeted at a page 315 if (isChecked(handler)) 316 { 317 IPageRequestHandler prh = (IPageRequestHandler)handler; 318 IRequestablePage targetedPage = prh.getPage(); 319 HttpServletRequest containerRequest = (HttpServletRequest)cycle.getRequest() 320 .getContainerRequest(); 321 String sourceUri = getSourceUri(containerRequest); 322 323 // Check if the page should be CSRF protected 324 if (isChecked(targetedPage)) 325 { 326 // if so check the Origin HTTP header 327 checkRequest(containerRequest, sourceUri, targetedPage); 328 } 329 else 330 { 331 if (log.isDebugEnabled()) 332 { 333 log.debug("Targeted page {} was opted out of the CSRF origin checks, allowed", 334 targetedPage.getClass().getName()); 335 } 336 allowHandler(containerRequest, sourceUri, targetedPage); 337 } 338 } 339 else 340 { 341 if (log.isTraceEnabled()) 342 log.trace( 343 "Resolved handler {} doesn't target an action on a page, no CSRF check performed", 344 handler.getClass().getName()); 345 } 346 } 347 348 /** 349 * Resolves the source URI from the request headers ({@code Origin} or {@code Referer}). 350 * 351 * @param containerRequest 352 * the current container request 353 * @return the normalized source URI. 354 */ 355 protected String getSourceUri(HttpServletRequest containerRequest) 356 { 357 String sourceUri = containerRequest.getHeader(WebRequest.HEADER_ORIGIN); 358 if (Strings.isEmpty(sourceUri)) 359 { 360 sourceUri = containerRequest.getHeader(WebRequest.HEADER_REFERER); 361 } 362 return normalizeUri(sourceUri); 363 } 364 365 /** 366 * Performs the check of the {@code Origin} or {@code Referer} header that is targeted at the 367 * {@code page}. 368 * 369 * @param request 370 * the current container request 371 * @param sourceUri 372 * the source URI 373 * @param page 374 * the page that is the target of the request 375 */ 376 protected void checkRequest(HttpServletRequest request, String sourceUri, IRequestablePage page) 377 { 378 if (sourceUri == null || sourceUri.isEmpty()) 379 { 380 log.debug("Source URI not present in request, {}", noOriginAction); 381 switch (noOriginAction) 382 { 383 case ALLOW : 384 allowHandler(request, sourceUri, page); 385 break; 386 case SUPPRESS : 387 suppressHandler(request, sourceUri, page); 388 break; 389 case ABORT : 390 abortHandler(request, sourceUri, page); 391 break; 392 } 393 return; 394 } 395 sourceUri = sourceUri.toLowerCase(Locale.ROOT); 396 397 // if the origin is a know and trusted origin, don't check any further but allow the request 398 if (isWhitelistedHost(sourceUri)) 399 { 400 whitelistedHandler(request, sourceUri, page); 401 return; 402 } 403 404 // check if the origin HTTP header matches the request URI 405 if (!isLocalOrigin(request, sourceUri)) 406 { 407 log.debug("Source URI conflicts with request origin, {}", conflictingOriginAction); 408 switch (conflictingOriginAction) 409 { 410 case ALLOW : 411 allowHandler(request, sourceUri, page); 412 break; 413 case SUPPRESS : 414 suppressHandler(request, sourceUri, page); 415 break; 416 case ABORT : 417 abortHandler(request, sourceUri, page); 418 break; 419 } 420 } 421 else 422 { 423 matchingOrigin(request, sourceUri, page); 424 } 425 } 426 427 /** 428 * Handles the case where an origin is in the whitelist. Default action is to allow the 429 * whitelisted origin. 430 * 431 * @param request 432 * the request 433 * @param origin 434 * the contents of the {@code Origin} HTTP header 435 * @param page 436 * the page that is targeted with this request 437 */ 438 protected void whitelistedHandler(HttpServletRequest request, String origin, 439 IRequestablePage page) 440 { 441 onWhitelisted(request, origin, page); 442 if (log.isDebugEnabled()) 443 { 444 log.debug("CSRF Origin {} was whitelisted, allowed for page {}", origin, 445 page.getClass().getName()); 446 } 447 } 448 449 /** 450 * Called when the origin was available in the whitelist. Override this method to implement your 451 * own custom action. 452 * 453 * @param request 454 * the request 455 * @param origin 456 * the contents of the {@code Origin} HTTP header 457 * @param page 458 * the page that is targeted with this request 459 */ 460 protected void onWhitelisted(HttpServletRequest request, String origin, IRequestablePage page) 461 { 462 } 463 464 /** 465 * Handles the case where an origin was checked and matched the request origin. Default action 466 * is to allow the whitelisted origin. 467 * 468 * @param request 469 * the request 470 * @param origin 471 * the contents of the {@code Origin} HTTP header 472 * @param page 473 * the page that is targeted with this request 474 */ 475 protected void matchingOrigin(HttpServletRequest request, String origin, 476 IRequestablePage page) 477 { 478 onMatchingOrigin(request, origin, page); 479 if (log.isDebugEnabled()) 480 { 481 log.debug("CSRF Origin {} matched requested resource, allowed for page {}", origin, 482 page.getClass().getName()); 483 } 484 } 485 486 /** 487 * Called when the origin HTTP header matched the request. Override this method to implement 488 * your own custom action. 489 * 490 * @param request 491 * the request 492 * @param origin 493 * the contents of the {@code Origin} HTTP header 494 * @param page 495 * the page that is targeted with this request 496 */ 497 protected void onMatchingOrigin(HttpServletRequest request, String origin, 498 IRequestablePage page) 499 { 500 } 501 502 /** 503 * Handles the case where an Origin HTTP header was not present or did not match the request 504 * origin, and the corresponding action ({@link #noOriginAction} or 505 * {@link #conflictingOriginAction}) is set to {@code ALLOW}. 506 * 507 * @param request 508 * the request 509 * @param origin 510 * the contents of the {@code Origin} HTTP header, may be {@code null} or empty 511 * @param page 512 * the page that is targeted with this request 513 */ 514 protected void allowHandler(HttpServletRequest request, String origin, 515 IRequestablePage page) 516 { 517 onAllowed(request, origin, page); 518 log.info("Possible CSRF attack, request URL: {}, Origin: {}, action: allowed", 519 request.getRequestURL(), origin); 520 } 521 522 /** 523 * Override this method to customize the case where an Origin HTTP header was not present or did 524 * not match the request origin, and the corresponding action ({@link #noOriginAction} or 525 * {@link #conflictingOriginAction}) is set to {@code ALLOW}. 526 * 527 * @param request 528 * the request 529 * @param origin 530 * the contents of the {@code Origin} HTTP header, may be {@code null} or empty 531 * @param page 532 * the page that is targeted with this request 533 */ 534 protected void onAllowed(HttpServletRequest request, String origin, IRequestablePage page) 535 { 536 } 537 538 /** 539 * Handles the case where an Origin HTTP header was not present or did not match the request 540 * origin, and the corresponding action ({@link #noOriginAction} or 541 * {@link #conflictingOriginAction}) is set to {@code SUPPRESS}. 542 * 543 * @param request 544 * the request 545 * @param origin 546 * the contents of the {@code Origin} HTTP header, may be {@code null} or empty 547 * @param page 548 * the page that is targeted with this request 549 */ 550 protected void suppressHandler(HttpServletRequest request, String origin, 551 IRequestablePage page) 552 { 553 onSuppressed(request, origin, page); 554 log.info("Possible CSRF attack, request URL: {}, Origin: {}, action: suppressed", 555 request.getRequestURL(), origin); 556 throw new RestartResponseException(page); 557 } 558 559 /** 560 * Override this method to customize the case where an Origin HTTP header was not present or did 561 * not match the request origin, and the corresponding action ({@link #noOriginAction} or 562 * {@link #conflictingOriginAction}) is set to {@code SUPPRESSED}. 563 * 564 * @param request 565 * the request 566 * @param origin 567 * the contents of the {@code Origin} HTTP header, may be {@code null} or empty 568 * @param page 569 * the page that is targeted with this request 570 */ 571 protected void onSuppressed(HttpServletRequest request, String origin, IRequestablePage page) 572 { 573 } 574 575 /** 576 * Handles the case where an Origin HTTP header was not present or did not match the request 577 * origin, and the corresponding action ({@link #noOriginAction} or 578 * {@link #conflictingOriginAction}) is set to {@code ABORT}. 579 * 580 * @param request 581 * the request 582 * @param origin 583 * the contents of the {@code Origin} HTTP header, may be {@code null} or empty 584 * @param page 585 * the page that is targeted with this request 586 */ 587 protected void abortHandler(HttpServletRequest request, String origin, 588 IRequestablePage page) 589 { 590 onAborted(request, origin, page); 591 log.info( 592 "Possible CSRF attack, request URL: {}, Origin: {}, action: aborted with error {} {}", 593 request.getRequestURL(), origin, errorCode, errorMessage); 594 throw new AbortWithHttpErrorCodeException(errorCode, errorMessage); 595 } 596 597 /** 598 * Override this method to customize the case where an Origin HTTP header was not present or did 599 * not match the request origin, and the corresponding action ({@link #noOriginAction} or 600 * {@link #conflictingOriginAction}) is set to {@code ABORTED}. 601 * 602 * @param request 603 * the request 604 * @param origin 605 * the contents of the {@code Origin} HTTP header, may be {@code null} or empty 606 * @param page 607 * the page that is targeted with this request 608 */ 609 protected void onAborted(HttpServletRequest request, String origin, IRequestablePage page) 610 { 611 } 612}