Cross-Site Request Forgery (CSRF) Protection

Duo 'Cancel this Request' issue affecting IdP v4.0.0 and v4.0.1

New installs of version 4.0.0 or 4.0.1 of the IdP (or upgrades from new installs of these versions) are missing the CSRF Token on the 'Cancel this Request' hyperlink in the duo.vm velocity template. This must be added manually, see duo-cancel-request. Note, this does not impact on the successful operation of the Duo 2FA process within the iframe.



Cross-Site Request Forgery (CSRF)

Cross Site Request Forgery (CSRF or XSRF) allows an attacker to forge a malicious cross origin write request to a targeted web application that invokes sensitive functions on behalf of an authenticated user. Forged requests are usually represented in the form of a hyperlink, zero-width image, in-page Javascript request, or auto-submitted form post. Some form of social engineering (e.g. a URL to a website in a phishing email), is used by the attacker to trick a victim into unknowingly submitting such a request.

CSRF exploits existing user session identifiers with ambient authority that are provided automatically by the browser during each request, e.g. cookies such as the JSESSIONID and shib_idp_session, or the IP address of the victim. As the attacker has no way of determining/reading the results of that request, useful requests are limited to state-changing, non-idempotent (has side effects) actions that the user is allowed to perform.

Despite the fact the IdP does not itself represent the end resource the user will ultimately interact with, the IdP does present some user flows that could be manipulated by an attacker. For example, of particular importance is the prevention of login CSRF [2], where the attacker attempts to trick the victim into logging in as themselves (the attacker), with the aim then of:

  1. Tracking a victim's activity because it is happening in their own session.

  2. Tricking the victim into entering sensitive information into an attacker's session/account e.g. bank account details etc.

Consequently, the IdP now employs a synchroniser token pattern [1] CSRF defense. Abstractly, a cryptographically strong string token is generated and attached to the user session by the IdP each time a view is rendered and embedded into HTML forms in the view. When the form is submitted by the user back to the IdP, the IdP verifies that the token in the request matches that stored against the user's session. If they do not match, it is assumed the request was forged - the attacker would not have access to this token, and should not be able to ‘guess’ it.

CSRF attacks on the IdP

Performing a CSRF attack on the IdP is difficult even without specialised CSRF defence. Firstly, for the most part, the IdP does not have stable, static, endpoints listening for requests (other than for certain RESTFul admin functions). Instead, because of the underlying use of Spring Web Flow, the conversation the browser is having with the IdP must be at the correct flow state to accept a HTTP POST/GET request with a given set of manipulatable parameters. For example, using the password authentication flow, the attack opportunity only occurs when the user has progressed the flow to the DisplayUsernamePasswordPage view-state (login page) and no further. It is fairly unlikely, although not impossible, a user would stop at this point and navigate to a malicious website to be tricked into submitting an attacker's username and password back to the IdP.

In addition, in order for Spring Web Flow to resume the flow conversation at the correct state to accept the request, the attacker must guess a suitable execution key. That said, a login would typically occur early in the conversation for a given protocol/binding, and would be fairly predictable/guessable. Finally, as with all login CSRF attacks, the attacker would need an account within the IdP’s authentication source.

However difficult, CSRF attacks are still possible, and these points do not detract from a robust CSRF defense by default.

Configuration

New installations

New installations of IdP v4 will have CSRF protection enabled by default. This is configured by a property in %{idp.home}/conf/idp.properties which globally turns on CSRF protection:

1 2 # Enable cross-site request forgery mitigation for views. idp.csrf.enabled = true

When enabled, all view-states will automatically be protected unless explicitly excluded; see excluding-views. This means any custom view-state added to new or existing flows of the IdP will also be protected, and any submissions back to the IdP will require an anti-CSRF token; see adding-csrf-token.



Duo 'Cancel this request' issue affecting IdP v4.0.0 and v4.0.1

New installs of, or upgrades from new installs of, version 4.0.0 or 4.0.1 of the IdP are missing the CSRF Token on the 'Cancel this request' hyperlink in the duo.vm velocity template. This must be added manually, see duo-cancel-request

Upgrades

On upgrade, user views and IdP properties files are not overwritten by the installer. Consequently, in an upgraded state from older versions of the IdP without this feature e.g. v3, CSRF protection is disabled by default, and views will not have the velocity template content required to add anti-CSRF tokens.

Enabling After Upgrade

The /dist folder in the installation directory of the IdP contains a fresh copy of the IdP properties and views with CSRF protection enabled. These could be referenced against your current views to highlight the changes required to enable CSRF.

To enable CSRF protection the following property should be added to %{idp.home}/conf/idp.properties:

1 2 # Enable cross-site request forgery mitigation for views. idp.csrf.enabled = true

Caution

Enabling CSRF protection and restarting the IdP without modifying required views will break authentication flows.

Once enabled, the following views need to be updated to include the anti-CSRF token inside HTML forms; see adding-csrf-token for more information on injecting the tokens.

View

Number of Forms
(in distribution)

Others

View

Number of Forms
(in distribution)

Others

views/login.vm

1



views/duo.vm

1

Is also required in the 'Cancel this request' hyperlink, see DuoCancelRequest.

views/intercept/attribute-release.vm

1



views/intercept/impersonate.vm

1



views/intercept/terms-of-use.vm

2



views/admin/unlock-keys.vm

1



The IdP contains a number of other views from which CSRF has been excluded by configuration; see excluding-views for more information on excluding views.

Excluding views

Sometimes views do not require CSRF protection, for example when they do not submit sensitive information back to the IdP. Other times, views are involved in user flows that cannot meaningfully benefit from ‘IdP led’ CSRF protection, for example, an external authentication servlet that reaches outside of the IdP and can include arbitrary user interactions. In either case, views can be excluded by annotating the <view-state> element with the following CSRF annotation attribute in the corresponding flow XML configuration file.

CSRF excluded annotation attribute
1 <attribute name="csrf_excluded" value="true" type="boolean"/>

For example, to exclude an external authentication servlet:

Example: exclude external authentication view
1 2 3 4 <view-state id="ExternalTransfer" view="externalRedirect:#{T(net.shibboleth.idp.authn.ExternalAuthentication).getExternalRedirect(flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.External.externalAuthnPathStrategy').apply(opensamlProfileRequestContext), flowExecutionContext.getKey().toString())}"> <attribute name="csrf_excluded" value="true" type="boolean"/> <!-- excludes attribute here --> ... </view-state>

Injecting anti-CSRF tokens into views

HTML forms

HTML forms within protected views must include a hidden input field with both the anti-CSRF token (value attribute) and the HTTP parameter name (name attribute). The IdP comes with a velocity template fragment which adds the anti-CSRF token into a form if present. To add it, simply place the following velocity script element inside the form where it is required:

1 #parse("csrf/csrf.vm")

For example, inside the login form:

1 2 3 4 <form action="$flowExecutionUrl" method="post"> #parse("csrf/csrf.vm") .... </form>

If protection is enabled, the token will then be rendered similar to the output below:



The csrf.vm template can be added (using the #parse directive) safely to forms even if protection is disabled. In these cases, Velocity will not add the token to the form.

Iframe source

Iframes that return control to the IdP in the source attribute also require CSRF tokens unless their parent view-state is excluded from protection. This is required even in a GET request as Spring Web Flow does not differentiate between GET and POST to initiate or resume flows. These can be added with conditional velocity logic e.g.

1 <iframe src="$flowExecutionUrl&_eventId=proceed#if($csrfToken)&${csrfToken.parameterName}=${csrfToken.token}#{else}#end"></iframe>

It is unlikely however that iframe usage benefits much from CSRF protection, and most often the view-state can be excluded. That said, the implementer of a custom flow and or view is responsible for determining that.

Token Exposure

This will expose the CSRF token in browser network inspectors and possibly server logs. However, it will not display in the navigation bar, and should not appear in browser history/bookmarks. That said, a new token is generated for every GET request to a view, greatly limiting its reply value.

Note, If you have upgraded to IdP v4.1.0 or later you can use the provided velocity fragment instead:

1 <iframe src="$flowExecutionUrl&_eventId=proceed#parse("csrf/csrf-qparam.vm")"></iframe>

For the same reason as the iFrame source above, if CSRF protection is enabled hyperlinks also require the CSRF token. These can be added using the same conditional velocity logic e.g.

1 <a href="$flowExecutionUrl&_eventId=proceed#if($csrfToken)&${csrfToken.parameterName}=${csrfToken.token}#{else}#end">TEXT</a>

Note, If you have upgraded to IdP v4.1.0 or later you can use the provided velocity fragment instead:

1 2 <a href="$flowExecutionUrl&_eventId=cancel#parse("csrf/csrf-qparam.vm")">TEXT</a>

Issue: Duo 2FA 'Cancel this Request'



In addition to the HTML form in the duo.vm view, the 'Cancel this Request' hyperlink also requires a CSRF token. That is, this hyperlink:

1 <a href="$flowExecutionUrl&_eventId=cancel">#springMessageText("idp.login.duoCancel", "Cancel this Request")</a>

Requires a CSRF token to be injected as a query parameter - which can be conditionally added as follows:

1 <a href="$flowExecutionUrl&_eventId=cancel#if($csrfToken)&${csrfToken.parameterName}=${csrfToken.token}#{else}#end">#springMessageText("idp.login.duoCancel", "Cancel this Request")</a>

Note, If you have upgraded to IdP v4.1.0 or later you can use the provided velocity fragment instead:

1 <a href="$flowExecutionUrl&_eventId=cancel#parse("csrf/csrf-qparam.vm")">#springMessageText("idp.login.duoCancel", "Cancel this Request")</a>



Custom Flows

If you add a new flow to the IdP, unless excluded (see excluding-views), any view-state it contains will automatically be protected from CSRF attacks providing CSRF protection is enabled globally in the IdP's properties file. Hence, the view template of any view-state must also include the anti-CSRF token; see inject-token.

To cleanly handle the InvalidCSRFTokenException the IdP throws on a CSRF token mismatch, a global on-exception transition must be registered in appropriate flows in addition to a corresponding action/end state. In subflows, this can be added to the list of error events that are reflected back to the parent flow. For example:

Catching a CSRF exception in subflows
1 2 3 4 5 6 7 <end-state id="InvalidCSRFToken" /> <global-transitions> <transition on-exception="net.shibboleth.idp.ui.csrf.InvalidCSRFTokenException" to="InvalidCSRFToken" /> <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" /> <!-- must be declared before this transition --> <transition on="InvalidCSRFToken" to="InvalidCSRFToken" /> ...

In abstract parent flows, this can be added to the global-transitions transitioning to a new action state:

Catching a CSRF exception in parent flows
1 2 3 4 5 6 7 8 9 10 11 12 <!-- action state to set the InvalidCSRFToken event and proceed to HandleError --> <action-state id="InvalidCSRFToken"> <evaluate expression="'InvalidCSRFToken'" /> <transition to="HandleError"/> </action-state> <!-- Default is to turn non-proceed events into an error. --> <global-transitions> <transition on-exception="net.shibboleth.idp.ui.csrf.InvalidCSRFTokenException" to="InvalidCSRFToken"/> <!--HERE--> <transition on-exception="java.lang.RuntimeException" to="LogRuntimeException" /> <transition on="#{!'proceed'.equals(currentEvent.id)}" to="HandleError" /> </global-transitions>

You are still protected from CSRF attacks even if you do not catch the exception, however, a generic uncaught exception error will be shown to the user instead.



References

1 - https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.md#synchronizer-token-pattern
2 - https://seclab.stanford.edu/websec/csrf/csrf.pdf