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.
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.
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.
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
(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.
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
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.
So if my analysis is correct, JSF 1.x offers weak protection against CSRF if you use server-side state saving, and nothing else.
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: