APIs enable different software systems to communicate and exchange data. However, without proper security measures, APIs can be vulnerable to attacks and threats. In this comprehensive guide, we explore the best practices for building secure APIs using the popular Node.js web framework Express.
Introduction
As web applications become more complex, the need for clean and well-defined APIs increases. APIs allow separating the frontend user interface from the server-side backend and enable easier integration across different platforms.
Node.js has emerged as a popular choice for building scalable network applications using JavaScript. The minimalist Express framework on top of Node provides handy features for building web APIs and handling HTTP requests/responses.
However, security is paramount while exposing APIs publicly over the internet. APIs are susceptible to common attacks like cross-site scripting, injection attacks, broken authentication etc. without proper precautions.
In this article, we will look at 10 easy and effective ways to implement security for Express APIs during development:
10 Best Practices for Building Secure APIs with Express.js
Here are 10 of the best practices to build secure REST APIs with Express.js:
1. Use Helmet Middleware
Helmet helps secure Express apps by setting various HTTP headers. For example, it can prevent sniffing and clicking XSS attacks via the X-XSS-Protection
header.
Install helmet:
npm install --save helmet
And configure it:
// index.js
const helmet = require('helmet')
app.use(helmet())
Helmet sets relevant security headers and is a simple first-line defence.
2. Validate Incoming Data
All user input data must be validated and sanitized before processing.
Use a validation library like Joi to define schemas and validate requests:
const Joi = require('joi')
const schema = Joi.object({
name: Joi.string().max(50).required(),
email: Joi.string().email()
})
app.post('/user', (req, res) => {
const { error } = schema.validate(req.body)
if (error) {
return res.status(400).send(error.details)
}
// process request body
})
This prevents malicious or malformed data from breaking the app.
3. Limit Requests Rate
To guard against brute force or DDoS attacks, use a middleware like express-rate-limit:
npm install express-rate-limit
Configure to allow a maximum of 100 requests per IP address:
const rateLimit = require('express-rate-limit')
const limiter = rateLimit({
windowMs: 1*60*1000, // 1 minute
max: 100
})
app.use(limiter)
The middleware blocks requests once the limit is reached.
4. Prevent Parameter Pollution
Attackers can try to exploit the app by sending duplicate query parameters like ?sort=asc&sort=desc
to cause issues.
Use dedupe middleware to remove duplicate params:
const dedupe = require('dedupe')
app.use(dedupe())
Now query params will contain unique values only, preventing pollution.
5. Encrypt Sensitive Data
Never store plain text passwords, payment details etc. Only securely hashed/encrypted data should be persisted.
Use bcrypt to hash passwords before storage:
const bcrypt = require('bcrypt')
const hash = await bcrypt.hash(password, 10)
For other data, use encryption modules like crypto for AES 256 encryption:
const crypto = require('crypto')
const ciphertext = crypto.createCipheriv(algorithm, key, iv)
Follow encryption best practices to maximize security.
6. Use JWTs for Stateless Auth
Session-based auth requires server-side state which makes horizontal scaling difficult.
Instead, use JSON Web Tokens to achieve stateless auth:
const jwt = require('jsonwebtoken')
app.post('/login', (req, res) => {
// authenticate credentials
const token = jwt.sign({ sub: userId }, secretKey)
res.json({ token })
})
app.get('/protected', ensureAuth, (req, res) => {
// send protected data
})
function ensureAuth(req, res, next) {
const bearerHeader = req.headers['authorization']
if (typeof bearerHeader !== 'undefined') {
const bearer = bearerHeader.split(' ')
const bearerToken = bearer[1]
req.token = bearerToken
} else {
res.sendStatus(401)
}
next()
}
JWT tokens contain the user identity and other data signed cryptographically.
7. Enable CORS Selectively
Browsers block Cross-Origin Resource Sharing (CORS) by default. Enable CORS with Careful restrictions:
app.use(cors({
origin: ['http://example.com'],
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type', 'Authorization']
}))
This allows example.com
to make requests while restricting other origins. Tweak origin, methods, headers etc. as per the client applications.
8. Add a Rate Limiting Middleware
Too many requests in a short period can indicate abuse or denial of service attacks.
Express middleware like express-brute can rate limit based on factors like IP address:
const ExpressBrute = require('express-brute')
const store = new ExpressBrute.MemoryStore()
const bruteforce = new ExpressBrute(store)
app.post('/auth',
bruteforce.prevent, // error 429 if too many requests
(req, res) => {
// auth handling
}
)
This protects the server from overloaded with requests.
9. Regularly Patch Dependencies
Outdated NPM packages with known vulnerabilities open up the app for attacks.
Use tools like npm audit
and Snyk to detect vulnerabilities:
$ npm audit
=== npm audit security report ===
# Run npm update webpack-dev-server --depth 3 to resolve 1 vulnerability
SEMVER WARNING: Recommended action is a potentially breaking change
Have a process to regularly update dependencies and patch security flaws.
10. Monitor for Suspicious Activity
Actively monitor logs and metrics for signs like sudden traffic spikes, too many 4xx/5xx responses etc. Use tools like:
Proactive monitoring lets you detect attacks before they create damage.
Conclusion
APIs enable the digital capabilities that power modern applications, but can also expose vulnerabilities.
By following these 10 Express.js security best practices during development, you can build robust APIs:
- Use Helmet and other middlewares
- Validate, sanitize, and encrypt data
- Rate limit requests
- Implement secure authentication
- Restrict CORS access
- Actively monitor for threats
Adopting these measures will ensure your APIs remain locked down and resistant to common web exploits. Paying attention to security from the start helps create safe and reliable APIs that users can trust.
Frequently Asked Questions
Here are some common questions about securing Express APIs:
What are some other methods to implement API auth besides JWT?
Some alternatives include API keys, OAuth 2.0, and HTTP Basic or Digest auth. Evaluate your needs to choose the right strategy.
How can I monitor my Express application for performance issues?
Use APM tools like New Relic or AppOptics that provide key metrics around requests, response times, errors etc. This helps identify bottlenecks.
Is there a way to implement fine-grained access control for Express APIs?
Look into using ACL (Access Control List) modules like node-casbin to enforce access rules, permissions and restrictions.
Should API security be tested manually or are automated tools better?
Utilize both. Automated scanners help uncover vulnerabilities at scale. Manual testing by experts mimics real-world attacks better and finds logical issues.
Is API security only a server-side concern or do clients need to take precautions too?
Clients should follow best practices around authentication, validation, encryption etc. as well. Security is an end-to-end concern for robust APIs.