Skip to content

Form Data

Express trusts whatever you throw at it. Every body , every field , every file is treated as innocent until proven malicious. That's not how security works
Form data comes in three flavors - urlencoded (standard HTML forms) , JSON (AJAX/API) , and multipart (file uploads). Express handles the first two natively. Multipart files need multer. Validation needs express-validator. Don't serve any of them without validation

urlencoded vs json - built-in body parsers

Express includes built-in body parsing since 4.16+

const express = require('express')
const app = express()

// parse JSON bodies - Content-Type: application/json
app.use(express.json({
  limit: '10kb'   // prevent large payload attacks
}))

// parse URL-encoded bodies - Content-Type: application/x-www-form-urlencoded
app.use(express.urlencoded({
  extended: true,   // true: supports nested objects via qs library
  limit: '10kb'
}))

Difference:

  • JSON - API requests with JSON payloads
  • urlencoded - standard HTML form submissions (name=value&name2=value2)
  • extended: false - uses query-string library (flat , no nested)
  • extended: true - uses qs library (supports nested: user[name]=mahmoud)

Payload size limits:

// prevent HTTP request smuggling and DoS attacks
app.use(express.json({ limit: '1mb' }))     // 1 megabyte max
app.use(express.urlencoded({ limit: '1mb' }))

// you'd be surprised how many people leave the default 100kb+ limit
// and wonder why their server OOMs under attack

multer - file uploads

Express doesn't handle multipart/form-data natively. That's what multer is for

npm install multer
const multer = require('multer')
const upload = multer({ dest: 'uploads/' })

// single file with field name 'avatar'
app.post('/profile' , upload.single('avatar') , (req , res) => {
  // req.file contains the uploaded file
  console.log(req.file)
  res.json({ uploaded: true })
})

req.file contents:

{
  fieldname: 'avatar',
  originalname: 'profile.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'uploads/',
  filename: 'd3f4a5b6c7e8f9a0b1c2d3e4f5a6b7c8',
  path: 'uploads/d3f4a5b6c7e8f9a0b1c2d3e4f5a6b7c8',
  size: 123456
}

multer - secure configuration

The default multer config is a security nightmare. Fix it:

const multer = require('multer')
const path = require('path')

// allowed file types only
const ALLOWED_TYPES = ['.jpg' , '.jpeg' , '.png' , '.gif' , '.pdf']
const MAX_SIZE = 5 * 1024 * 1024  // 5MB

const storage = multer.diskStorage({
  destination: (req , file , cb) => {
    cb(null , 'uploads/')
  },
  filename: (req , file , cb) => {
    // NEVER use original filename - path traversal risk
    const ext = path.extname(file.originalname).toLowerCase()
    if (!ALLOWED_TYPES.includes(ext)) {
      return cb(new Error(`File type ${ext} not allowed`))
    }
    // generate random filename - no user-controlled names
    const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${ext}`
    cb(null , uniqueName)
  }
})

const fileFilter = (req , file , cb) => {
  const ext = path.extname(file.originalname).toLowerCase()
  if (ALLOWED_TYPES.includes(ext)) {
    cb(null , true)
  } else {
    cb(new Error(`File type ${ext} not allowed`) , false)
  }
}

const upload = multer({
  storage,
  fileFilter,
  limits: { fileSize: MAX_SIZE }
})

form validation with express-validator

Body parsers parse the data. They don't validate it

npm install express-validator
const { body , validationResult } = require('express-validator')

app.post('/users',
  [
    body('email')
      .isEmail()
      .normalizeEmail(),        // sanitize - lowercase , remove dots
    body('password')
      .isLength({ min: 8 })
      .withMessage('Password must be at least 8 characters'),
    body('age')
      .optional()
      .isInt({ min: 0 , max: 150 })
      .toInt()                  // sanitize - cast to number
  ],
  (req , res) => {
    // check for validation errors
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() })
    }

    // proceed with validated data
    const { email , password , age } = req.body
    res.json({ created: true })
  }
)

sanitization

Validation catches bad data. Sanitization scrubs it

const { body } = require('express-validator')

app.post('/profile',
  [
    body('username')
      .trim()                    // remove whitespace
      .escape()                  // convert HTML entities
      .isLength({ min: 3 , max: 30 }),
    body('bio')
      .trim()
      .escape()
      .isLength({ max: 500 }),
    body('website')
      .trim()
      .isURL({ protocols: ['https'] }),  // only HTTPS URLs
    body('email')
      .normalizeEmail()           // lowercase Gmail , remove dots , etc
  ],
  handler
)

validation error responses

Consistent error format saves your frontend team from guessing

app.post('/register' ,
  [
    body('email').isEmail().withMessage('Valid email required'),
    body('password').isLength({ min: 8 }).withMessage('Password 8+ chars'),
    body('confirmPassword').custom((value , { req }) => {
      if (value !== req.body.password) {
        throw new Error('Passwords must match')
      }
      return true
    })
  ],
  (req , res) => {
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
      return res.status(422).json({  // 422 Unprocessable Entity
        error: 'Validation failed',
        details: errors.array().map(e => ({
          field: e.path,
          message: e.msg
        }))
      })
    }
    // create user...
  }
)

common form data attacks

// ATTACK 1: Payload too large
// Send 100MB JSON body - server OOMs
// FIX: Set express.json({ limit: '1mb' })

// ATTACK 2: Multipart bomb
// Upload 1000 files in one request - fills disk
// FIX: multer({ limits: { files: 5 } })

// ATTACK 3: Malicious filename
// originalname: '../../../etc/passwd'
// FIX: Never use originalname as storage path

// ATTACK 4: Type confusion
// Send JSON with string where number expected
// FIX: Validate types with express-validator or Zod

prerequisites

express_07_templating.md - template engines , res.render() , partials


next → express_09_cookies.md