Best practices form processing with Express
Asked Answered
J

3

6

I'm writing a website which implements a usermanagement system and I wonder what best practices regarding form processing I have to consider.

Especially performance, security, SEO and user experience are important to me. When I was working on it I came across a couple questions and I didn't find an complete node/express code snippet where I could figure out all of my below questions.

Use case: Someone is going to update the birthday of his profile. Right now I am doing a POST request to the same URL to process the form on that page and the POST request will respond with a 302 redirect to the same URL.

General questions about form processing:

  1. Should I do a POST request + 302 redirect for form processing or rather something else like an AJAX request?
  2. How should I handle invalid FORM requests (for example invalid login, or email address is already in use during signup)?

Express specific questions about form processing:

  1. I assume before inserting anything into my DB I need to sanitize and validate all form fields on the server side. How would you do that?

  2. I read some things about CSRF but I have never implemented a CSRF protection. I'd be happy to see that in the code snippet too

  3. Do I need to take care of any other possible vulnerabilities when processing forms with Express?

Example HTML/Pug:

form#profile(method='POST', action='/settings/profile')
    input#profile-real-name.validate(type='text', name='profileRealName', value=profile.name)
    label(for='profile-real-name') Name

    textarea#profile-bio.materialize-textarea(placeholder='Tell a little about yourself', name='profileBio')
        | #{profile.bio}
    label(for='profile-bio') About

    input#profile-url.validate(type='url', name='profileUrl', value=profile.bio)
    label(for='profile-url') URL

    input#profile-location.validate(type='text', name='profileLocation', value=profile.location)
    label(for='profile-location') Location

    .form-action-buttons.right-align
        a.btn.grey(href='' onclick='resetForm()') Reset
        button.btn.waves-effect.waves-light(type='submit')

Example Route Handlers:

router.get('/settings/profile', isLoggedIn, profile)
router.post('/settings/profile', isLoggedIn, updateProfile)

function profile(req, res) {
    res.render('user/profile', { title: 'Profile', profile: req.user.profile })
}

function updateProfile(req, res) {
    var userId = req.user._id
    var form = req.body
    var profile = {
        name: form.profileRealName,
        bio: form.profileBio,
        url: form.profileUrl,
        location: form.profileLocation
    }

    // Insert into DB
}

Note: A complete code snippet which takes care of all form processing best practices adapted to the given example is highly appreciated. I'm fine with using any publicly available express middleware.

Jocelin answered 11/5, 2017 at 17:35 Comment(0)
S
3

Should I do a POST request + 302 redirect for form processing or rather something else like an AJAX request?

No, best practice for a good user experience since 2004 or so (basically since gmail launched) has been form submission via AJAX and not web 1.0 full-page load form POSTs. In particular, error handling via AJAX is less likely to leave your user at a dead end browser error page and then hit issues with the back button. The AJAX in this case should send an HTTP PATCH request to be most semantically correct but POST or PUT will also get the job done.

How should I handle invalid FORM requests (for example invalid login, or email address is already in use during signup)?

Invalid user input should result in an HTTP 400 Bad Request status code response, with details about the specific error(s) in a JSON response body (the format varies per application but either a general message or field-by-field errors are common themes)

For email already in use I use the HTTP 409 Conflict status code as a more particular flavor of general bad request payload.

I assume before inserting anything into my DB I need to sanitize and validate all form fields on the server side. How would you do that?

Absolutely. There are many tools. I generally define a schema for a valid request in JSON Schema and use a library from npm to validate that such as is-my-json-valid or ajv. In particular, I recommend being as strict as possible: reject incorrect types, or coerce types if you must, remove unexpected properties, use small but reasonable string length limits and strict regular expression patterns for strings when you can, and of course make sure your DB library property prevents injection attacks.

I read some things about CSRF but I have never implemented a CSRF protection.

The OWSAP Node Goat Project CSRF Exercise is a good place to start with a vulnerable app, understand and exploit the vulnerability, then implement the fix (in this case with a straightforward integration of the express.csrf() middleware.

Do I need to take care of any other possible vulnerabilities when processing forms with Express?

Yes generally application developers must understand and actively code securely. There's a lot of material out there on this but particular care must be taken when user input gets involved in database queries, subprocess spawning, or being written back out to HTML. Solid query libraries and template engines will handle most of the work here, you just need to be aware of the mechanics and potential places malicious user input could sneak in (like image filenames, etc).

Scarecrow answered 16/5, 2017 at 18:11 Comment(2)
Thanks for taking the time, do you mind showing a real world code example which demonstrates how you are sanitazing and validating a form? That would help me understanding your approach. I'm using mongoose in case you wonder.Jocelin
I dislike mongoose quite a lot, so sorry no.Scarecrow
M
1

I am certainly no Express expert but I think I can answer at least #1:

You should follow the Post/Redirect/Get web development pattern in order to prevent duplicate form submissions. I've heard a 303-redirect is the proper http statuscode for redirecting form submissions.

I do process forms using the POST route and once I'm done I trigger a 302-redirect.

As of #3 I recommend looking into express-validator, which is well introduce here: https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/forms . It's a middleware which allows you to validate and sanitize like this:

req.checkBody('name', 'Invalid name').isAlpha();
req.checkBody('age', 'Invalid age').notEmpty().isInt();
req.sanitizeBody('name').escape();

I wasn't able to comment hence the answer even though it's not a complete answer. Just thought it might help you.

Milanmilanese answered 11/5, 2017 at 17:48 Comment(0)
H
1

If user experience is something you're thinking about, a page redirection is a strong no. Providing a smooth flow for the people visiting your website is important to prevent drops, and since forms are already not such a pleasure to fill, easing their usage is primary. You don't want to reload their page that might have already took some time to load just to display an error message. Once the form is valid and you created the user cookie, a redirection is fine though, even if you could do things on the client app to prevent it, but that's out-of-scope.

As stated by Levent, you should checkout express-validator, which is the more established solution for this kind of purpose.

req.check('profileRealName', 'Bad name provided').notEmpty().isAlpha()
req.check('profileLocation', 'Invalid location').optional().isAlpha();

req.getValidationResult().then(function (result) {
  if (result.isEmpty()) { return null }
  var errors = result.array()
  // [
  //   { param: "profileRealName", msg: "Bad name provided", value: ".." },
  //   { param: "profileLocation", msg: "Invalid location", value: ".." }
  // ]
  res.status(400).send(errors)
})
.then(function () {
  // everything is fine! insert into the DB and respond..
})

From what it looks like, I can assume you are using MongoDB. Given that, I would recommend using an ODM, like Mongoose. It will allow you to define models for your schemas and put restrictions directly on it, letting the model handles these kind of redundant validations for you.

For example, a model for your user could be

var User = new Schema({
  name: { type: String, required: [true, 'Name required'] },
  bio: { type: String, match: /[a-z]/ },
  age: { type: Number, min: 18 }, // I don't know the kind of site you run ;)
})

Using this schema on your route would be looking like

var user = new User({
  name: form.profileRealName,
  bio: form.profileBio,
  url: form.profileUrl,
  location: form.profileLocation
})

user.save(function (err) {
  // and you could grab the error here if it exists, and react accordingly
});

As you can see it provides a pretty cool api, which you should read about in their docs if you want to know more.

About CRSF, you should install csurf, which has pretty good instructions and example usages on their readme.

After that you're pretty much good to go, there is not much more I can think about apart making sure you stay up to date with your critical dependencies, in case a 0-day occurs, for example the one that happened in 2015 with JWTs, but that's still kinda rare.

Hazard answered 15/5, 2017 at 3:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.