Common Web Application Security Vulnerabilities and How to Fix Them

Most web application breaches are not the work of exotic zero-days. They come from a short list of well-understood mistakes that show up again and again, year after year. The OWASP Top 10 tracks these, and the 2021 edition is still a good map of where applications go wrong: broken access control, injection, and security misconfiguration sit near the top, with cryptographic failures, vulnerable dependencies, and server-side request forgery close behind.

The good news is that every one of these has a known fix. This post walks through the most common vulnerabilities, shows what they look like in code, and covers how to prevent them. The examples use JavaScript and Node, but the ideas apply to any stack.

1. Broken Access Control

Broken access control is the number one risk on the OWASP list, and it is easy to see why. Authentication answers "who are you?" Access control answers "what are you allowed to do?" and that second question has to be asked on every single request.

The classic mistake is an insecure direct object reference (IDOR), where the server trusts an ID supplied by the client:

// Any logged-in user can read any invoice by changing the ID in the URL
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
  const invoice = await db.invoices.findById(req.params.id)
  res.json(invoice)
})

If I am logged in as user 41 and request /api/invoices/1001, nothing checks that invoice 1001 is mine.

How to fix it

  • Enforce authorization on the server, always. Hiding a button in the UI is not access control.
  • Scope queries to the current user. Make ownership part of the query rather than a check you might forget.
  • Deny by default. New endpoints should require explicit permission, not explicit restriction.
  • Centralize the logic. A policy layer or middleware is easier to audit than checks scattered across handlers.
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
  const invoice = await db.invoices.findOne({
    id: req.params.id,
    ownerId: req.user.id,
  })

  if (!invoice) return res.sendStatus(404)
  res.json(invoice)
})

Returning 404 instead of 403 for resources the user does not own also avoids confirming that the resource exists.

2. Injection

Injection happens when untrusted input is mixed into a command or query and interpreted as code. SQL injection is the best-known example, but the same pattern applies to NoSQL queries, OS commands, LDAP, and template engines.

// Vulnerable: user input becomes part of the SQL itself
const rows = await db.query(
  `SELECT * FROM users WHERE email = '${req.body.email}'`,
)

An attacker who submits ' OR '1'='1 as the email changes the meaning of the query. With a little more effort they can read other tables or modify data.

How to fix it

  • Use parameterized queries. The database receives the query and the values separately, so input can never become syntax.
  • Use an ORM or query builder carefully. They parameterize by default, but raw-query escape hatches reintroduce the risk.
  • Validate input against an allow-list. Check type, length, and format. Validation is a second line of defense, not a replacement for parameterization.
  • Avoid shelling out. If you must run a command, pass arguments as an array (execFile) instead of building a string for a shell.
const rows = await db.query('SELECT * FROM users WHERE email = $1', [
  req.body.email,
])

3. Cross-Site Scripting (XSS)

XSS occurs when an application includes untrusted data in a page without proper escaping, letting an attacker run JavaScript in other users' browsers. That script can steal session data, perform actions as the victim, or rewrite the page to phish credentials.

There are three flavors: stored (the payload is saved, such as in a comment), reflected (the payload comes from the request, such as a search term), and DOM-based (client-side code writes unsafe data into the page).

// Dangerous: interprets the string as HTML
element.innerHTML = comment.body

How to fix it

  • Let your framework escape output. Vue, React, and Angular escape interpolated values by default. Most XSS in modern apps comes from bypassing that protection with v-html, dangerouslySetInnerHTML, or innerHTML.
  • Sanitize when you truly need HTML. If users can submit rich text, run it through a maintained sanitizer such as DOMPurify rather than writing your own filter.
  • Set a Content Security Policy (CSP). A strict CSP limits where scripts can load from and blocks inline script execution, which turns many XSS bugs into non-events.
  • Mark session cookies HttpOnly. Even if script runs, it cannot read the cookie.
// Safe: treated as text, not markup
element.textContent = comment.body

4. Broken Authentication and Session Management

Weak authentication lets attackers take over accounts without needing to find a bug in your code. Credential stuffing, where leaked passwords from other sites are replayed against yours, is automated and constant.

Common problems include no rate limiting on login, weak password rules, session IDs that never expire, and session tokens that are not rotated after login.

How to fix it

  • Hash passwords with a slow, purpose-built algorithm. Use Argon2id, scrypt, or bcrypt. Never store plain text, and never use a fast hash like SHA-256 for passwords.
  • Offer multi-factor authentication. Passkeys or TOTP dramatically reduce the impact of stolen passwords.
  • Rate limit and monitor login attempts. Slow down repeated failures per account and per IP, and alert on unusual patterns.
  • Handle sessions carefully. Generate IDs with a cryptographically secure random source, rotate them on login and privilege changes, expire them, and invalidate them on logout.
  • Set cookie flags. Use HttpOnly, Secure, and an appropriate SameSite value.
  • Do not roll your own. Use a well-maintained auth library or provider where you can.

5. Cross-Site Request Forgery (CSRF)

CSRF tricks a logged-in user's browser into sending an unwanted request to your site. Because browsers attach cookies automatically, a malicious page can submit a form to yourbank.com/transfer and the request arrives with the victim's valid session.

How to fix it

  • Use SameSite cookies. SameSite=Lax (the default in modern browsers) or Strict stops cookies from being sent on most cross-site requests.
  • Use anti-CSRF tokens for state-changing requests, especially if you support older browsers or need SameSite=None.
  • Never change state on GET requests. Links and image tags can trigger them.
  • Check the Origin header on sensitive requests as an additional layer.

APIs that authenticate with an Authorization header instead of cookies are generally not exposed to CSRF, because the browser does not attach that header automatically.

6. Security Misconfiguration

A secure application can still be undermined by how it is deployed. Misconfiguration is a broad category, and it includes things like:

  • Default credentials left on admin panels or databases
  • Debug mode and verbose stack traces enabled in production
  • Publicly readable cloud storage buckets
  • Unnecessary ports, services, or HTTP methods left open
  • Missing security headers

How to fix it

  • Harden and automate your environments. Infrastructure as code makes configuration reviewable and repeatable.
  • Disable debug output in production and return generic error messages to users while logging details privately.
  • Apply least privilege to service accounts, database users, and cloud roles.
  • Add security headers. At a minimum, consider Content-Security-Policy, Strict-Transport-Security, X-Content-Type-Options: nosniff, and Referrer-Policy.
  • Scan regularly. Tools such as Mozilla Observatory can check your headers in seconds.

7. Sensitive Data Exposure and Cryptographic Failures

Data that should be protected often is not: passwords in logs, API responses that return entire user records, unencrypted backups, or traffic sent over plain HTTP.

How to fix it

  • Use HTTPS everywhere and enable HSTS so browsers refuse to downgrade.
  • Encrypt sensitive data at rest and manage keys properly, outside your code and repository.
  • Return only what the client needs. Do not serialize a whole database row and hope the frontend ignores the sensitive fields.
  • Do not collect data you do not need. Data you never stored cannot be leaked.
  • Use standard cryptographic libraries and algorithms. Do not invent your own, and avoid outdated ones like MD5 and SHA-1 for security purposes.
  • Keep secrets out of source control. Use environment variables or a secrets manager, and rotate anything that has been committed by mistake.

8. Vulnerable and Outdated Dependencies

Modern applications are mostly other people's code. A typical project pulls in hundreds or thousands of transitive packages, and any one of them can contain a known vulnerability, or be compromised outright in a supply chain attack.

How to fix it

  • Audit regularly. Run npm audit (or your ecosystem's equivalent) and automate it in CI.
  • Automate updates. Dependabot or Renovate keep you moving instead of falling years behind.
  • Commit your lockfile and install from it in CI so builds are reproducible.
  • Be selective. Every dependency is attack surface, so prefer well-maintained packages and remove ones you no longer use.
  • Review before upgrading blindly. Pay attention to new maintainers, unexpected install scripts, and suspicious version bumps.

9. Server-Side Request Forgery (SSRF)

SSRF occurs when your server fetches a URL supplied by a user, such as a webhook, an image import, or a link preview, and an attacker points it at something internal. In cloud environments, a classic target is the instance metadata service, which can hand out credentials to anyone who can reach it from inside the network.

// Dangerous: the server will fetch whatever URL it is given
const response = await fetch(req.body.url)

How to fix it

  • Allow-list destinations where possible instead of trying to block bad ones.
  • Resolve and validate the IP address before connecting, and reject private, loopback, and link-local ranges. Re-check after redirects, or disable redirects.
  • Isolate the fetching service on a network segment with no access to internal systems.
  • Require modern metadata protections such as IMDSv2 on AWS.

10. Insufficient Logging and Monitoring

When something does go wrong, the difference between a contained incident and a catastrophe is often how quickly you noticed. Breaches commonly go undetected for months because nobody was looking.

How to fix it

  • Log security-relevant events: logins, failures, permission denials, password resets, and administrative actions.
  • Never log secrets. Passwords, tokens, and full card numbers do not belong in logs.
  • Centralize and alert. Logs nobody reads are not monitoring. Alert on spikes in failed logins, unusual access patterns, and error rates.
  • Have an incident response plan before you need one.

Building Security In

Looking at this list, a few themes repeat:

  1. Never trust input. Treat everything from the client, including headers, cookies, and hidden form fields, as attacker-controlled.
  2. Use the secure default. Frameworks and libraries exist so you do not have to hand-write escaping, hashing, and session handling. Fighting their defaults is where bugs creep in.
  3. Apply least privilege. Users, services, and tokens should have only the access they need.
  4. Layer your defenses. Validation, parameterization, CSP, and monitoring each catch what the others miss.
  5. Automate the boring parts. Dependency scanning, linting for dangerous patterns, and header checks belong in CI so they run every time.

Security is not a feature you add at the end. It is a set of habits applied from the first line of code. Start with the items above, add automated checks to your pipeline, and make threat modeling a regular part of design reviews. You will not eliminate risk entirely, but you will close the doors that attackers walk through most often.