[seam-dev] JSF and CSRF

Dan Allen dan.j.allen at gmail.com
Wed Mar 11 02:46:08 EDT 2009


Christian's concerns are valid as demonstrated in the example scenarios I
provided. After giving it some thought, I believe I have arrived at a
solution, or perhaps at least the starting point of a solution. I want to
first comment on a couple of things, then get to the proposed solution.


> We have two contradicting objectives, we can't have it all automagically.
>
> 1. You want forms on a webpage that are submitted via POST, independent of
> any session state on the server. Typically these are login or comment forms,
> or any view that users might keep open for a long time until the session
> expires, and then trigger a POST request that should be valid.


Also consider the case where the login information is submitted with the
POST (common on blogging sites) or the session just turned over (meaning in
another tab the user logged out and logged in again).


> 2. You don't want POST requests accepted by the server that execute an
> action without any forgery protection. Otherwise you are vulnerable to CSRF
> attacks. Common remedies are bound to the user's session in one way or
> another. The best protection is offered by strong random tokens in hidden
> form fields that are validated against the stored value in the user's
> session. Weaker but still acceptable protection would be duplicate sending
> of the session identifier - in the cookie by the browser AND as a request
> parameter or hidden POST data. An attacker would have to guess either the
> random token value or the session identifier to forge a request.


I think the best thing to do is just use a separate token which can
optionally bind to the session id when created.


>
> JSF (and probably Seam) need to offer you tools so you can balance these
> goals on a per-case basis.
>
> First consider JSF 1.x:
>
> You can globally configure client-side state saving. If you are saving the
> view serialized on the client, and the server is configured to accept this
> serialized state of a view without any further privilege validation, your
> application is vulnerable to CSRF. So while you get objective 1, it
> completely ignores objective 2. Worse, it's a global switch that affects
> _all_ of your forms/views. Also, it doesn't matter that the client state is
> saved in a "complex" hidden form value, an attacker can forge that too. The
> server will accept it as long as it's well-formed and can be deserialized.


You are correct. The complexity of the string is just smoke and mirrors. The
attacker only has to realize that it is the same serialized string is used
every time to realize he has found a back door.


>
>
> To protect against tampering (!NOT! CSRF) the Sun JSF RI offers encryption
> of the client-side state with a configurable global password. See
> http://wiki.glassfish.java.net/Wiki.jsp?page=JavaServerFacesRI#section-JavaServerFacesRI-HowCanISecureViewStateWhenUsingClientSideStateSaving -
> The issue here is that the view state cyphertext in the hidden form field
> also comes with at least part of the original plaintext, making it easier to
> crack the global password, see last comment on
> https://javaserverfaces.dev.java.net/issues/show_bug.cgi?id=906


As Christian points out, this solution is purely for scrambling the View
State and should not be mistaken for a secure token of any sort.


>
> (I never really bothered with client-side state saving so the following
> might be wrong.) I don't think client-side state saving can be made secure
> against CSRF attacks without losing its main benefit. Consider the following
> "replay" attack procedure:
>
> - Attacker goes to vulnerable website and grabs the encrypted view state of
> the hidden field in a comment form.
> - Attacker goes to vulnerable website and posts a new comment, linking to
> his own website.
> - Victim goes to the vulnerable website and follows the link to the
> attackers website.
> - Victim clicks on a button on the attackers website.
> - A forged POST request will be send to the vulnerable website, riding the
> victims still active session.
> - The forged POST request contained the valid encrypted view state
> (decrypted with a global password), and the attackers data.
>
> The only protection against this kind of attack would be a session
> identifier included in the view state (or at least in the request outside of
> the cookie), which means we don't get session independence of the form/view!
> Another protection would be globally incremented component identifiers, so
> that an old view state with "lower" values than the current can not be
> replayed. This might be a way out but I'm not sure at this point how secure
> it really is. After all, developers can assign their own client component
> identifiers for e.g. JavaScript access.


Actually, I think this is the right idea. My proposal builds on this.


>
> You can globally configure server-side state saving. The server will throw
> a ViewExpiredException if a POST request arrives without a valid (present in
> session) view identifier. This completely ignores objective 1 but offers
> some protection against CSRF. Unfortunately, the JSF RI generates a
> predictable view identifier. This needs to be fixed, see
> https://javaserverfaces.dev.java.net/issues/show_bug.cgi?id=812


Either way, the predictable view identifier should be fixed. However, once
our token idea is in place, using a server-side view state will just be
gravy on top of the protection already in place...meaning good for very
secure and paranoid sites, but we can still be reasonable secure without
this approach.


>
> You can globally configure Facelets BUILD_BEFORE_RESTORE option for
> server-side state saving. This, like client-side state saving, gets you
> objective 1 but again on a global basis. Worse, the attacker doesn't even
> have to forge the potentially complex serialized view state, break the
> encryption, or mount a replay attack. Definitely not recommended.


Yes, build before restore should only be used once another secure token
strategy is in place. I am proposing one below. Even then, you should only
use this for more open sites (like blogs and forums) and not for bank sites
(which should always limit session length and be paranoid about idle and
reestablished sessions).


> So if my analysis is correct, JSF 1.x offers weak protection against CSRF
> if you use server-side state saving, and nothing else.


JSF 1.x is very hackable.


>
> For JSF 2.x this is what we need: A _non-global_ per-form switch to define
> how the view state should be restored. These would be my recommendations:


Here's what I came up with building on Christian's recommendations. Feel
free to offer improvements.

[ UIToken / <s:token> ]

My idea is to introduce a per-form secure token. Let me first explain why it
is secure, then how it works.

The first step is to assign a unique identifier to the browser in the form
of a cookie (javax.faces.ClientUid). This is similar to the session id, only
it is established once for each browser session (until the user closes the
browser). As you may know, there is no dependable way to identify a browser.
So we are assigning it unique identifier so that there is. This unique
identifier will be used as a salt when generating the token and can only be
known by the browser (never visible to an attacker under normal
circumstances).

The next step is to generate a token for the form, which is a hashed version
of the view signature. That hash is calculated as follows:

sha1( signature = viewId + "," + formClientId, salt = clientUid )

The developer can also choose to incorporate the session id into this hash
for a more secure token (at the cost of binding it to the session)

sha1( signature = viewId + "," + formClientId + "," + sessionId, salt =
clientUid )

That token is then stored in a hidden field of the form

<input type="hidden" name="javax.faces.FormSignature"
value="HASH_GOES_HERE"/>

All of that is accomplished using the <s:token> component tag:

<h:form>
    ...
    <s:token requireSession="true or false"/>
</h:form>

When the form is submitted, the token is checked in the decode() method of
UIToken. Here's how it is checked:

1. assert this is a postback, otherwise skip the check
2. assert that this form was the one that was submitted, otherwise skip the
check
3. get the brower Uid, otherwise throw an exception that the browser must
have a Uid (it should if it rendered a page having <s:token>)
4. get the javax.faces.FormSignature request parameter, otherwise throw an
exception that the form signature is missing
5. generate the hash as before and assert it equals the value of the
javax.faces.FormSignature request parameter, otherwise throw an exception

If all of that passes, we are okay to process the form (advance to validate
phase as decode() is called in apply request values)

OPEN QUESTION: should we create a special exception for an invalid form
signature, like ViewExpiredException?

Please note that this token can be combined with client-side state saving or
the "build during restore" strategy to unbind a POST from the session that
created the view -- without sacrificing security. However, it's still the
most secure to require the view state to be present in the session (JSF 1.2
server-side state saving).

Feedback welcome. I'll follow up with a patch.

-Dan

-- 
Dan Allen
Senior Software Engineer, Red Hat | Author of Seam in Action

http://mojavelinux.com
http://mojavelinux.com/seaminaction

NOTE: While I make a strong effort to keep up with my email on a daily
basis, personal or other work matters can sometimes keep me away
from my email. If you contact me, but don't hear back for more than a week,
it is very likely that I am excessively backlogged or the message was
caught in the spam filters.  Please don't hesitate to resend a message if
you feel that it did not reach my attention.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.jboss.org/pipermail/seam-dev/attachments/20090311/7082c734/attachment.html 


More information about the seam-dev mailing list