Submitted by Corey Pennycuff on
Part 2: Forms—A better way
Part 3: Defiant—A step toward unification
Forms—A better way
The better form processing that I propose is inspired by Drupal, but furthers the idea to improve on it. In order to properly understand the approach, though, you should understand how Cryptographic Signing (specifically, Authenticated Encryption) and the Galois Counter Mode (GCM) operate.
A quick overview of the GCM cipher goes like this: Given a secret key, a plaintext, and an initialization vector (IV), a GCM cipher should return a ciphertext and an authentication code. The ciphertext, IV, and authentication code is then given to the user. When the user returns, the GCM decrypt function takes in the secret key, the ciphertext, the IV, and the authentication code, and returns the plaintext. This may sound a bit complicated, so let me break it down a bit more.
The secret key is something that is stored serverside, and is preferably unique to each user. The IV is a short string that is unique to that encryption. The uniqueness is a requirement to a counter-type of encryption like GCM. In short, the IV must be different every time that the encryption is called, otherwise the ciphertext is vulnerable. Lastly, the authentication code is a string which validates that the contents of the encrypted text has not been tampered with. It does not matter if the end user is given the encrypted text, IV, and authentication code, because if the user changes any part of any one of these, then they will not validate when combined with the secret key.
In order to put this knowledge to use, we must decide what we are trying to accomplish. My list is as follows:
- CSRF protection
- The ability to re-constitute a form (without saving it to a database).
- The ability to give encrypted data to the user and ensure its unaltered return.
- The ability to give unencrypted data to the user and ensure its unaltered return.
- Prevent the user from omitting parts of the form.
- Prevent the user from swapping parts of the form with another form that they have been given.
- Expiring forms
Just as before, we will take these points in turn.
CSRF protection
CSRF protection is not magic. It is only a verification that the token that the user returns (via a hidden element in the form) was, in fact, generated for that user. For the purposes of this approach, the CSRF token is not a token at all in the conventional sense, but rather a JSON-formatted array containing the following elements: The form ID (plaintext), the IV, the authentication code, and lastly an encrypted portion (to be discussed in later parts). The stringified JSON contents are then put into a hidden form element, which we will call the Validation element. As an aside, you may notice that I did not discuss having both plaintext and encrypted text together, but many cipher libraries (such as the Node.js Crypto library) support using the authentication code to sign both the plaintext and the encrypted text simultaneously, meaning that changes to either the plaintext or the encrypted text will be detected via the authentication code.
If the element can be properly interpreted as JSON and the cryptographic signing is validated, then we can be assured that it was generated from our script, for this form, for this user. It has served the purpose of a CSRF token. What about the encrypted text, though? The encrypted text, when decrypted, is a another stringified JSON object, which contains various other types of data, to be discussed in the subsequent parts.
The ability to re-constitute a form (without saving it to a database).
A form can be reconstituted either by rebuilding it from a common starting state (my preferred method), or by storing the final form in some way. I prefer the rebuilding method. Rebuilding a form is not necessarily hard, so long as a deterministic process is used to create the form and the starting state is known. When a form is built, the factors important to the build (for example, the id of the piece of content being edited, and anything else that is necessary to generate an exact copy of the form) should be saved. On the other hand, Drupal's approach is to store a copy of the final form structure in the database. I propose that either the final form structure or the build state itself (depending on the philosophy of your framework) be included as a part of the encrypted portion of our Validation element.
Because the encrypted portion cannot be changed (because of the authorization code), and cannot be read by the end user (because of the unique IV and the secret key), it is a secure process for retrieval. Furthermore, the build state is linked with the form ID through the authorization code, meaning that one cannot be separated from the other and still validate. It is a form of secure enclave.
The ability to give encrypted data to the user and ensure its unaltered return.
This is trivial to implement in my proposal, but generally unheard of in the frameworks that I surveyed. The idea is that encrypted text can be placed into a hidden form element, but that the associated IV (remember, each and every encryption must have a unique IV, even in the same form) and authentication code are placed into the encrypted portion of the Validation element.
The advantages to this system are as follows: the encrypted text cannot be separated from the form's Validation element (meaning you can't pull an encrypted element from another form and put it in place of the current element). Also, the encrypted text gets the same benefit of using a GCM encryption with validation.
The ability to give unencrypted data to the user and ensure its unaltered return.
Giving unencrypted text is similar to the encrypted element in that the plaintext is sent to the user in a hidden form element (which I refer to as a Static element), and that the unique IV for this element and the authentication code are also stored in the encrypted text of the Validation element. The advantages listed in the previous section also apply to its unencrypted counterpart.
Prevent the user from omitting parts of the form.
The ease of verifying that elements may not be omitted may be found in simply including an array of required elements into the encrypted text of the Validation element. Once again, the information cannot be altered without invalidating the form. This, paired with the encrypted or plaintext hidden elements, means that the form structure can be extended with confidence, knowing that the form will return exactly the same information that it was generated with, otherwise it will fail validation.
Prevent the user from swapping parts of the form with another form that they have been given.
This last section has been hinted at in the previous sections, but is significant enough to warrant further discussion. The act of including the unique IVs, the authentication codes, the "required" elements list, and the build information inside the encrypted area of the validation element results in a secure pairing of information. A malicious user cannot "mix-and-match" elements from different forms as an injection attack. They cannot alter the contents of the secured elements without invalidating the form. As far as I have seen, this is the most secure way to package secure form contents, while only allowing the user to change the parts that are meant to be changed.
Expiring forms
Creating expiring forms is trivial given the above techniques. One can put the expiration time in a Static element (so that the expiration can be observed by the end user, in a script, perhaps), and yet it cannot be altered. The expiration Static element can be required, as discussed in a previous section, so that it cannot be omitted. The end result is a form that expires at a certain time in the future, individual to that form, and is guaranteed to be accurate if the validation passes.
You may notice, however, that we have not addressed the problem of server-side validation or of those mischievous file uploads alluded to earlier. That is because they require a more sophisticated solution, one which I will propose in the next section.