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