Web App Security Essentials: A Practical Guide Based on the OWASP Top 10
The security risks that matter most for web developers, explained with practical examples and fixes: access control, injection, authentication, misconfiguration, dependencies and more.
When you build software for real organisations — a pharmacy system handling sales, a learning platform with user accounts, an admin dashboard for a community programme — security stops being an abstract topic. Real people trust you with their data.
The best starting point I know is the OWASP Top 10, a widely used awareness document from the Open Worldwide Application Security Project that describes the most critical categories of web application security risks. OWASP updates the list periodically, and the categories get renamed, merged and reordered between editions, so always check owasp.org for the current version. The underlying lessons, however, are remarkably stable.
In this post I'll group the key risks into practical themes and show what each looks like in everyday code.
1. Broken access control: the most important one to get right
Access control means making sure users can only do what they are allowed to do. It sits at or near the top of the OWASP list for good reason: it is easy to get wrong and the consequences are serious.
A classic mistake looks like this:
// Dangerous: trusts whatever ID the client sends
app.get("/api/orders/:id", async (req, res) => {
const order = await db.orders.findById(req.params.id);
res.json(order);
});Any logged-in user can change the ID in the URL and see someone else's order. The fix is to check ownership on the server, every time:
app.get("/api/orders/:id", requireAuth, async (req, res) => {
const order = await db.orders.findById(req.params.id);
if (!order || order.userId !== req.user.id) {
return res.status(404).json({ error: "Not found" });
}
res.json(order);
});Key habits:
- Deny by default. Grant access explicitly.
- Enforce on the server, never only by hiding buttons in the UI.
- Check ownership and roles on every request that touches data.
- In serverless apps (like Firebase), your security rules are your access control — test them.
2. Injection: never mix data and commands
Injection happens when user input is treated as code — in SQL queries, shell commands or HTML.
SQL injection
// Vulnerable
const sql = "SELECT * FROM users WHERE email = '" + email + "'";
// Safe: parameterised query
const [rows] = await db.execute(
"SELECT * FROM users WHERE email = ?",
[email]
);Parameterised queries (or a reputable ORM / query builder used correctly) keep data and commands separate.
Cross-site scripting (XSS)
XSS is injection into the browser. If you insert user input as HTML, an attacker can run scripts in other users' browsers.
// Risky with user input
comment.innerHTML = userComment;
// Safe
comment.textContent = userComment;If you must render user-provided HTML or Markdown, use a well-maintained sanitiser library, and add a Content Security Policy (CSP) header as a second layer of defence. Modern frameworks escape output by default, but escape hatches (like rendering raw HTML) bypass that protection.
3. Authentication failures
Weak authentication lets attackers take over accounts. Practical defences:
- Never store plain-text passwords. Use a slow, salted hashing algorithm designed for passwords, such as bcrypt or Argon2.
- Rate-limit login attempts to slow down guessing attacks.
- Offer multi-factor authentication (MFA), especially for admin accounts.
- Don't reveal which part was wrong. "Invalid email or password" is better than "No account with that email."
- Manage sessions securely: set cookies with
HttpOnly,Secureand an appropriateSameSitevalue; expire sessions; invalidate them on logout and password change. - Consider a trusted auth provider (such as Firebase Authentication or a similar service) rather than building everything from scratch.
4. Cryptographic failures: protect sensitive data
- Use HTTPS everywhere — no exceptions.
- Don't collect sensitive data you don't need. Data you never store can never leak.
- Encrypt sensitive data at rest where appropriate.
- Never put secrets (API keys with real privileges, database passwords) in frontend code or in your Git repository. Use environment variables and secret managers.
5. Security misconfiguration
Many breaches have nothing to do with clever hacking — just something left open.
- Default credentials not changed
- Debug mode or detailed error messages enabled in production
- Database or storage buckets left publicly readable
- Missing security headers
- Unused features, ports or admin pages left exposed
A minimal set of headers worth knowing:
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-originA strict CSP will usually need adjusting for your actual scripts, fonts and APIs — start strict and loosen carefully.
6. Vulnerable components and the software supply chain
Modern apps depend on many third-party packages. Each one is code you didn't write but are responsible for. The OWASP community has put increasing emphasis on this area, and for good reason.
- Run
npm audit(or your ecosystem's equivalent) and act on serious findings. - Enable automated dependency update alerts on your repository.
- Remove packages you don't use.
- Be cautious with new or obscure packages; check maintenance activity and watch out for names that imitate popular packages.
- Commit your lockfile so builds are reproducible.
7. Insecure design
Some vulnerabilities aren't coding mistakes — they are design gaps. For example: a password reset flow with no expiry, a discount code that can be applied unlimited times, or a POS system where a cashier can change prices without any record.
Before building a feature, ask: "How could someone misuse this?" Write down a few abuse cases alongside your user stories. This simple habit, often called threat modelling, catches problems before they become code.
8. Logging, monitoring and handling errors
If something goes wrong, will you know?
- Log important security events: logins, failed logins, permission denials, admin actions.
- Never log passwords, tokens or full payment details.
- Set up alerts for unusual activity.
- Handle errors gracefully: show users a friendly message, log details privately, and make sure failures don't leave the system in an unsafe state (for example, granting access when a check throws an error).
9. Server-side request forgery (SSRF)
If your server fetches URLs provided by users (for previews, imports or webhooks), an attacker might trick it into requesting internal resources. Validate and restrict destinations with an allow-list, and avoid fetching arbitrary user-supplied URLs where you can.
Security checklist for your next project
- Every data request checks authentication and authorization on the server
- All database queries are parameterised
- User content is rendered as text, or sanitised; CSP is in place
- Passwords are hashed with a password-hashing algorithm; MFA available for admins
- HTTPS everywhere; secure cookie flags set
- No secrets in frontend code or Git history
- Production has no debug mode, default credentials or public buckets
- Dependencies are audited and updated regularly
- Abuse cases considered during design
- Security events are logged, without sensitive data
Conclusion
You don't need to be a security specialist to build secure software. You need good habits: never trust input, enforce access on the server, keep secrets secret, update your dependencies and think about misuse before you build. The OWASP Top 10 is a great map — read the current edition, and revisit it every time you start a new project.
Which of these areas do you find hardest to get right in your projects? Let me know in the comments — I'm happy to go deeper on any of them in a future post.

0 Comments