CSRF Mitigation Options
Work in Progress
Work In Progress
Introduction and Context
Here we look at three options for integrating a synchroniser token pattern[1] Cross Site Request Forgery (CSRF) defence into the IdP's Password authentication flow (although it could be configured more widely e.g. other authn views or admin views).
Login forms can be susceptible to CSRF[2] attacks. In these cases the attacker attempts to trick the victim into logging in as themselves (the attacker). The attacker could then:
- Track a victims activity because it is happening in their own session.
- Trick the victim into entering sensitive information into an attackers session/account e.g. bank account details etc.
Appendix A describes an example Login CSRF attack on the IdP.
Option 1 - ViewScoped CSRF Token
Overview
Add a cryptographically secure anti-csrf token to the request context viewScope
on-entry
to any view-state. Then check the returned token (in the HTTP request) matches that stored in the viewScope
on a proceed event/
transition. If the token is invalid, prevent execution of the transition and re-render the view, else proceed.
Advantages of Approach:
- per-view (as on-entry, per request if on-render) synchroniser token - marginally safer than per-session or conversation etc. but more expensive. Although with the IdP only typically having a few views per flow, the efficiency/safety tradeoff is probably less of an issue.
- the token only needs to be stored for the lifetime of the view state.
- the token is only generated on view states, and is not generated when not needed e.g. when using a previous AuthenticationResult.
Disadvantages of Approach:
- is specific to SWF e.g. SWF request context and view-scopes. Although inevitably the token needs to be somewhere in the session for velocity to evaluate it.
- has to be repeated for each view-state in the configuration.
- no guarantees this approach will be adhered to if custom flows with custom views are created.
- tied to the synchoniser token pattern, can not be configured to use cookies over, essentially, the HttpSession, to store tokens.
Possible high level implementation issues
- the validation action is nested within a SWF transition. These are limited to events that allow transitions to proceed (eventids of ‘yes’,‘success’, and ‘true’), or not proceed (any other eventId) which re-renders the view. Consequently:
- detection of an invalid CSRF token can not trigger further transition to a globally defined error end-state, which would terminate the authentication attempt. Instead, the flow is resumed at the login view until a valid CSRF token is supplied.
- a suitable CSRF error can be displayed if encoded inside an AuthenticationErrorContext by the validation action. Although this would have to be encoded differently for say admin views post authentication.
- the (current) validation action is-a type of
AbstractProfileAction
. Which under every other instance, produces a meaningful SWF event that triggers a new state via a transition - this therefore seems like a missue of this type of action, or its contract.
- detection of an invalid CSRF token can not trigger further transition to a globally defined error end-state, which would terminate the authentication attempt. Instead, the flow is resumed at the login view until a valid CSRF token is supplied.
Implementation
The proof of concept can be found on my personal git repository [git@git.shibboleth.net:philsmart/java-identity-provider] (branch feature/anti-xsrf-token-viewscope
).
New Classes
- A
CsrfToken
API interface . This defines token implementations that return token values and HTTP parameter names.- packaged inside
idp-session-api
. Although this maybe better inside ofjava-support, or idp-ui
?
- packaged inside
- A default implementation of CsrfToken, namely
SimpleCsrfToken
. Encapsulating both token value and HTTP parameter name as a non-empty Strings. Has no business logic.- packaged inside
idp-session-api
. Although this maybe better inside ofjava-support, or idp-ui
?
- packaged inside
- A
CsrfTokenManager
to provide helper methods for generating and validating anti-csrf tokens .- packaged inside
idp-session-impl
. Although this maybe better inside ofjava-support, or idp-ui
? - uses a configurable
SecureRandomIdentifierGenerationStrategy
to generate anti-csrf tokens - even though suitable, is generating a token and not an identifier. - currently fixed to a single implementation.
- packaged inside
- A
ValidateCsrfToken
action class. Extracts the CsrfToken from therequestContext
viewScope
and the CSRF token String value from the HTTP request. Delegates to theCsrfTokenManager
to compare them, if equal (String comparison) signals a ‘success’ event, if different signals a ‘failure’ event. Note here, as this action is (see later sections) nested within a transaction, any event with ID other than ‘success’, ‘yes’, or ‘true’ will prevent the transaction from executing.- is-a
AbstractProfileAction
, is not anAbstractValidationAction
- it does not produce anAuthenticationResult
.
- is-a
Global config changes
The CsrfTokenManager is added to the set of global system beans (global-system.xml):
<!-- Cross Site Request Forgery token manager --> <bean id="shibboleth.CsrfTokenManager" class="net.shibboleth.idp.session.impl.CsrfTokenManager" p:csrfParameterName="%{idp.csrf.token.parameter:csrf_token}"/>
Flow beans
The ValidateCsrfToken
prototype bean must be declared where it is used (e.g. password-authn-beans.xml, or perhaps more globally when used across flow types)
<bean id="ValidateCsrfToken" class="net.shibboleth.idp.session.impl.ValidateCsrfToken" scope="prototype" p:httpServletRequest-ref="shibboleth.HttpServletRequest" p:csrfTokenManager-ref="shibboleth.CsrfTokenManager" />
Changes required for web flow view-state
The anti-csrf token is generated on-entry
(or on-render) to a view state, and placed inside the SWF viewScope. For example in the DisplayUsernamePasswordPage view-state of the authn-password-flow.xml:
<on-entry> <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CsrfTokenManager').generateCsrfToken()" result="viewScope.csrfToken" /> </on-entry>
Still inside the DisplayUsernamePasswordPage view-state, the anti-csrf token stored in the viewScope must be compared to that returned as a HTTP parameter from the client. This needs to happen (because the token is bound to the viewScope) inside the view-state
before the proceed
transition is executed and the state exited. Hence, a ValidateCsrfToken
action is nested inside the proceed
transition:
<transition on="proceed" to="ExtractUsernamePasswordFromFormRequest"> <evaluate expression="ValidateCsrfToken"/> <!-- CSRF Validation Action --> <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).setAttemptedFlow(thisFlow)" /> </transition>
If token validation fails, SWF will not execute the transition, and the view will be re-rendered with appropriate error message.
View Changes
Any HTML Forms must include a hidden input field with both the anti-csrf token ( value
attribute), and the HTTP parameter name (name
attribute).
<form action="$flowExecutionUrl" method="post"> <input type="hidden" name="${csrfToken.parameterName}" value="${csrfToken.token}" id="csrf_token"> ... </form>
This will then be rendered, for example, in the output as:
Option 2 - FlowExecutionListener Injected ViewScoped CSRF Token
Implementation of this has moved to here.
Option 3 - Add a CSRF token to a new CSRFUI context
Overview
Add a cryptographically secure anti-csrf token to a CSRF subcontext of the ProfileRequestContext (initialised early in the authentication flow at present). The returned CSRF token from a view-state in the HTTP request is checked against that stored in the context by a suitable profile action. The conversation ends and a suitable error message is displayed if an invalid CSRF token is found.
Advantages of Approach:
- Per conversation anti-csrf token stored in the context tree.
- Set once, accessible by any view-state thereafter.
- Can be validated outside the view-state by any action - provided that action occurs before any response is sent to the client e.g. before additional view-states.
- Better architectural fit with IdP context tree and action components.
- Clear from flow definitions how CSRF protection is configured.
Disadvantages of Approach:
- per conversation token is marginally less safe than per view - although this does not seem significant for the IdP, as it has a limited number of view-states, and limited conversation durations.
- initialisation and validation actions would need to be configured/integrated slightly differently for different flows e.g.the admin flow .
- has to be incorporated, where needed, into each flow with view-state.
- not clear at this stage where to place the CSRF validation action. In its own action-state, or inside another etc.
- no guarantees this approach will be adhered too if custom flows with custom views are created.
- small overhead of creating/managing a context and holding onto state.
- Although a single token can safely be used for a number of view-states if required.
Implementation
The proof of concept can be found on my personal git repository [git@git.shibboleth.net:philsmart/java-identity-provider] (branch feature/anti-xsrf-token-context
).
New Classes
All new classes have been packaged inside the idp-ui
module.
- A
CsrfToken
interface. This defines token implementations that return token values and HTTP parameter names. - A default implementation of
CsrfToken
, namelySimpleCsrfToken
. Encapsulating both token value and HTTP parameter name as a non-empty String. Has no business logic. - A new IdP context for holding per conversation CSRF tokens,
CsrfUIContext
. - An action,
InitializeCsrfUIContext
, to initlialise theCsrfUIContext
, generate aCsrfToken
using aSecureRandomIdentifierGenerationStrategy
and configured HTTP Parameter name, and set it onto the context. - A
ValidateCsrfToken
action class. Takes the CsrfToken from the CsrfUIContext and compares it against that extracted from the HTTP request.- is-a AbstractProfileAction, is not an AbstractValidationAction - it does not produce an AuthenticationResult.
- return a new
IdPEventIds.INVALID_CSRF_TOKEN
eventID if token validation fails, else return nothing (null) to allow other actions to proceed. - map that eventID to an end-state and the default error view with custom error message.
Global config changes
Signify an InvalidCsrfToken
event is ‘local’ by adding it to the shibboleth.LocalEventMap
in errors.xml
.
<entry key="InvalidCSRFToken" value="false" />
Flow beans
The InitializeCsrfUIContext
prototype bean is declared in the authn-beans.xml
so it can be used to initalise the CsrfUIContext in the AuthenticationSetup action-state of the authn-flow.xml
. e.g. in authn-beans.xml
<bean id="InitializeCsrfUIContext" class="net.shibboleth.idp.ui.csrf.impl.InitializeCsrfUIContext" scope="prototype" p:csrfParameterName="csrf_token"/>
The ValidateCsrfToken prototype bean must be declared where it is used (e.g. password-authn-beans.xml)
<bean id="ValidateCsrfToken" class="net.shibboleth.idp.ui.csrf.impl.ValidateCsrfToken" scope="prototype" p:httpServletRequest-ref="shibboleth.HttpServletRequest"/>
Changes To Flows
CsrfUIContext Initialisation
The InitializeCsrfUIContext
action is added to the AuthenticationSetup action-state in the authn-flow.xml
.
<action-state id="AuthenticationSetup"> <evaluate expression="PopulateAuthenticationContext" /> <evaluate expression="PopulateSessionContext" /> <evaluate expression="SetRPUIInformation" /> <evaluate expression="InitializeCsrfUIContext"/> <evaluate expression="'proceed'" /> <transition on="proceed" to="TestForSession" /> </action-state>
Error end-state and transition
To correctly transition the IdPEventIds.INVALID_CSRF_TOKEN
event ID to an error page, a transition needs to be placed in the action-state where the validation profile action is evaluated (see next section), and a global transition needs to be defined that maps to an end-state.
For example, in the authn-abstract-flow.xml
:
<end-state id="InvalidCsrfToken" /> <global-transitions> ... <transition on="InvalidCsrfToken" to="InvalidCsrfToken" /> ... </global-transitions>
Changes to password authn flow
The CSRF token is taken from the CsrfUIContext
and placed inside the SWF viewScope on-render of the DisplayUsernamePasswordPage view-state (or any view-state as required):
<view-state id="DisplayUsernamePasswordPage" view="login"> <on-render> ... <evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.ui.csrf.context.CsrfUIContext)).getCsrfToken()" result="viewScope.csrfToken" /> </on-render> ... </view-state>
Note, as the profile request context is already passed into the view, the CsrfUIContext and token could be extracted by the view templating engine e.g. as is the case with the RelyingPartyContext in the login view.
The CSRF token in the HTTP request is validated against that stored in the CSRF UI context using the ValidateCsrfToken
profile action. This action could, as with option 1, be placed inside the ‘proceed’ transition. However, in order to cleanly transition to an end-state and default error view, it has been placed outside the view-state, and inside an action-state - in this case the ExtractUsernamePasswordFromFormRequest
action-state.
<action-state id="ExtractUsernamePasswordFromFormRequest"> <evaluate expression="ValidateCsrfToken" /> <evaluate expression="ExtractUsernamePasswordFromFormRequest" /> <evaluate expression="'proceed'" /> <transition on="InvalidCSRFToken" to="InvalidCSRFToken" /> <!-- Let the validate action handle any problems later. --> <transition to="ValidateUsernamePassword" /> </action-state>
An additional transition is required to handle the invalid CSRF token event - this can not be made global in this case, as the *->ValidateUsernamePassword
transition would catch it first.
Importantly, it needs more thought as to which action-state to use, and whether modification to the flow (e.g. new action-state) would be more appropriate.
View Changes
Any HTML Forms must include a hidden input field with both the anti-csrf token ( value
attribute), and the HTTP parameter name (name
attribute).
<form action="$flowExecutionUrl" method="post"> <input type="hidden" name="${csrfToken.parameterName}" value="${csrfToken.token}" id="csrf_token"> ...
This will then be rendered, for example, in the output as:
Error View Message Customisation
The default error view can then be customised to show a more appropriate error message on an InvalidCSRFToken event. For example, in system/messages/messages.properties
:
InvalidCSRFToken = invalid-csrf-token ... invalid-csrf-token.title= Login Failed invalid-csrf-token.message= CSRF token verification failed.
Other Ideas
- The options above are not mutually exclusive. For example, a flow execution lister could be used to inject tokens into the SWF viewScope, while a profile action could be used to check it. Similarly, a profile action could be used to initialise a CSRF Context, an SWF action be used to add it to the viewScope, and a flow execution listener used to check it on ‘proceed’ transition.
Appendix A - Login CSRF Example
Login CSRF Example
The following HTML form is an example of one which can be used by an attacker to post a username and password to the IdP. This is essentially that which is used by the IdP's login view, but pre-populated with a username and password of the attacker - whom must have an active account with the IdP's authentication source.
<form action="https://localhost:8443/idp/profile/SAML2/Unsolicited/SSO?execution=e1s2" method="post"> <input id="username" name="j_username" type="text" value="jdoe"/> <input id="password" name="j_password" type="password" value="PASSWORD"/> <button type="submit" name="_eventId_proceed">Login</button> </form>
Such a form could be auto-submitted without the user knowing using JavaScript.
For the Password IdP flow, the victim using a web browser user agent must:
- Initiate a password authentication flow at the IdP using a valid SAML authentication request e.g. attempt a login to a service provider etc.
- Note, a victim could be tricked into beginning the authentication process to the IdP the attacker has an account with using some form of wayfless URL e.g. IdP initiated unsolicited SSO or SP Request Initiator.
- Receive a response relating to the `login` (DisplayUsernamePasswordPage) spring webflow view-state.
- In the process creating a session with the IdP, tracked by a JSESSIONID cookie.
- The JSESSIONID cookie is essential for the malicious HTML form post attack to link the victim back to their session and the specific point in the webflow conversation. It is this view-state (keyed by execution and snapshot) which extracts (using the `ExtractUsernamePasswordFromFormRequest` action) the `j_username` and `j_password` POST parameters on the event/transition `proceed` (as referenced by the submit button name).
- In the process creating a session with the IdP, tracked by a JSESSIONID cookie.
- Not submit their username and password via the login page. Instead, within that same browser session, navigate elsewhere to be tricked into submitting a HTML form (e.g. the one above) which posts an attackers username and password to the correct flow endpoint e.g. idp/profile/SAML2/Redirect/SSO?, using the correct execution and snapshot keys e.g. ?execution=e1s2
Risk (low):
- Such an attack is made difficult (in a good way) by the fact the IdP does not have an explicit and static endpoint listening for login requests. Instead the conversation the browser is having with the IdP must be at the correct flow state to accept a HTTP Post request with username and password parameters.
- Remembering the CSRF request is blind to the attacker, so if the attacker did start the SSO process for a victim, they would not see the result, and the attacker is not in control of what the victim does next.
- The attacker must guess a suitable execution key - although a login would typically happen early in a users conversation with the IdP for a given protocol/binding, and would be fairly predictable.
- The attack opportunity only occurs when the user has progressed the password authentication flow to the `DisplayUsernamePasswordPage` view-state and no further. It is fairly unlikely, although not impossible, a user would stop at this point.
- The attacker would require an account with the IdP they intended to trick a user into using.