DEV Community

Cover image for Handling HTML form security
Amer Sikira
Amer Sikira

Posted on • Originally published at webinuse.com

Handling HTML form security

This post was originally posted on webinuse.com
During my career as a web developer, I created millions of different HTML forms. I remember the first website hack I have ever experienced was through HTML form. I am not saying that the following advice will protect you from everything, but it will give you enough security (in my experience) that you can relax. Still, my advice is to always follow the latest code standards, use recent versions of programming language, regularly update your code, plugins, and everything else.

1. Use proper tags

HTML is fluid language. We could build almost everything with only three tags: <div>, <a>, <input/>, but that does not mean we should. The first level of security is to actually utilize the power of HTML by using proper tags. E.g. if you need an email field use input type=”email” because it already has built-in verification and even if JavaScript is turned off in the browser, it will still work.

More about the form we can find here.

    <!-- This is not good -->
    <input type="text" name="email" id="email" class="input-field" placeholder="Please enter your email" />

    <!-- This is much better -->
    <input type="email" name="email" id="email" class="input-field" placeholder="Please enter your email" />
Enter fullscreen mode Exit fullscreen mode

2. Use multi-level validation

I always prefer to validate forms on multiple levels. The first level is HTML, by using proper tags and attributes on each of those tags, the second level is usually JavaScript, and the third level is in the backend.

The first level of validation. Since we are expecting users to insert a number of items we will use input type number, and also we will use min and max attributes to restrict the number of items users can pick, but also to validate the user’s input.

    <!-- User can pick items, no less than 1 and no more than 10\. -->
    <label for="number">Pick number of items</label>
    <input type="number" name="number" id="number" class="input-field" min="1" max="10">
Enter fullscreen mode Exit fullscreen mode

After the user picked a number, I like to set my validation on focusout event, because I want the user to react immediately, I do not want to show a full screen of errors when a user submits the form. But also I would check for the same things because if the user managed to bypass HTML validation like if the user used the console to alter the code, I want to validate it.

    function validateNumberField(e) {
       //First I would use parseInt() to clean code of everything 
       //apart from intergers (which is what we are expecting)
       let value = parseInt(e.target.value);
       if (!value || isNaN(value)) {
          //We can notify user here and than we return false
          return false;
       }

       if (value < 1 || value > 10) {
          //Notify user
          return false;
       }

       //If everything is ok, we can return whatever we want
       return true;
    }

    document.querySelector("#number").addEventListener("focusout", validateNumberField);
Enter fullscreen mode Exit fullscreen mode

After the user submitted the form, we are going to check for the same things in the backend. Since we already used JavaScript, I am going to use PHP for the backend, to show validation from as many different angles as possible.

    /* Let's say we used post request to send our number to back
     * and want to make sure that we recieve only number */
    $number = intval($_POST['number']);

    if (empty($number) || !is_numeric($number)) {
       //Send status code and response
       return false;
    }

    if ($number < 1 || $number > 10) {
       //Send status code and response
       return false;
    }

    //Return success
    return true;
Enter fullscreen mode Exit fullscreen mode

Also, you if you store any of form into the database you should set validation there, by using proper field types. For example, if you use MySQL and you need to store an integer you should use INT, MEDIUMINT, or BIGINT as a field type.

It is worth mentioning that if user input is dependable on values from the database you should also cross-check those.

Let’s say that those items the user was picking were items from your webshop. You don’t want users to buy more items than you have in your stock, so an additional check would be:

    /* Let's say we used post request to send our number to back
     * and want to make sure that we recieve only number */
    $number = intval($_POST['number']);

    /*In this example we will add an ID number so that we can check database */
    $id = intval($_POST['id'];

    //If we do not have ID it is pointless to continue
    if (empty($id) || !is_numeric($id)) { return false; }

    if (empty($number) || !is_numeric($number)) {
       //Send status code and response
       return false;
    }

    if ($number < 1 || $number > 10) {
       //Send status code and response
       return false;
    }

    $stmt = $pdo->prepare("SELECT stock FROM product WHERE id = ?");
    $stmt->execute([$id]);
    $stock = $stmt->fetch();

    if ($number < $stock['stock']) {
       //There isn't enough items in the stock return status code and 
       //response
       return false;
    }
    //Return success
    return true;
Enter fullscreen mode Exit fullscreen mode

3. Use CSRF token

CSRF is a secret, unpredictable, random set of characters created by the server-side and sent to a client, so that client can, later, verify its identity and/or session. CSRF is usually created using a secret key and timestamp, although we can include some user-specific stuff into this algorithm.

What does this all mean? It means that when the user signs into your application you assign a unique CSRF token to him/her and save this token somewhere server-side, like session, file, database, etc. Every time user makes a request to the back-end (especially if this request needs data or sends data) this CSRF will be sent in the request so the server can verify the user.

    /*We use secret key that needs to be long and really secret :D*/
    $secret = 'jfaskWERfvajsdlklkj$#$%#jklavclkny324341rRESAvcfa...';

    /*Than we hash our csrf with some irreversible hash, so the algorithm behind can not be uncovered*/
    $csrfToken = hash('sha256', $secret . time());

    /*We need to save token for further use*/
    $_SESSION['csrf_token'] = $csrfToken;
Enter fullscreen mode Exit fullscreen mode

On the front-end, we can save this CSRF token in a hidden input field or in a cookie. So when a user submits a form you can perform a check if the CSRF user sent and the one you have saved on the server-side is the same. TIP: Use === for comparison

4. Use Captcha

Form security is not always about data validation, sometimes it's about user validation. Use Captcha on forms that do not require a login, like contact forms. Also, you can use Honeypot fields. Honeypot fields are basically hidden from fields that must remain empty. This is important because of the bots, most of the bots on the internet don’t know which field is hidden so it will fill all fields. If the hidden field is filled then most likely it is spam.

    <style>
    .hidden-field {
       display: none;
    }
    </style>

    <!-- You should use type like email or text, so bot thinks it's something valuable, do not use hidden fields -->
    <input type="email" name="email-1" class="hidden-field">
Enter fullscreen mode Exit fullscreen mode

IMHO the best Captcha is reCaptcha by Google and you can read on it here

5. Validate logic

A friend of mine had e-commerce where you could buy some stuff and then you choose what payment method you want. If you pick to pay when the product is delivered, your order would be converted to an invoice then he would print it and send it with the order. Nice, right? Well, the problem was pointed out to him by another friend of ours that he never checked if the logic of that order was ok, besides having some other security risks.

Explanation

He had products at the price of 10, so if you order 10 of those, the total should be 100. The problem was that he never checked for that in the backend. So, when our friend posted an order, then using Postman, he stopped the request after it was sent from a browser (more info), and ordered 10 items for the price of one.

This is just one example and I am sure that there are more of them out there.

6. Additional

I also like to check the Origin Header when receiving requests. It is just one more step. It is nothing super safe, it just adds an additional layer of security, and I’ve learned that every layer counts.

I saw some situations where programmers tend to check if the request was direct, or with some async functions, methods like AJAX, fetch(), etc. But this is not really reliable due to browsers being browsers.

DISCLAIMER This is not Holy text regarding form security, there is probably better and safer stuff to do. There is probably some stuff I forgot to mention. I would like this text to be a guideline, not an axiom.

I am, also, aware that code in this text is pretty simple and primitive, but this text is for those who want to learn about form security, but they are in the beginning. Also, I want this text to be accessible to everyone, not only to those who are into coding for longer periods of time.

Top comments (0)