Skip to main content
โšก Calmops

Web Security Fundamentals: A Developer's Guide

Introduction

Web security is not optional โ€” it’s a core part of building software. Every web application is a potential target, and the most common vulnerabilities are well-documented and preventable. This guide covers the essential concepts, the OWASP Top 10, and practical defenses you can apply today.

OWASP: The Security Standard

The OWASP Cheat Sheet Series is the go-to reference for application security. It provides concise, actionable guidance on specific topics written by security professionals.

The OWASP Top 10 is the most widely referenced list of critical web application security risks. Understanding it is the baseline for any web developer.

OWASP Top 10 (2021)

1. Broken Access Control

Users can act outside their intended permissions โ€” accessing other users’ data, admin pages, or performing unauthorized actions.

# Bad: user ID comes from the request, not the session
GET /api/users/1234/profile

# Good: derive the user from the authenticated session
GET /api/me/profile

Defenses:

  • Enforce access control server-side on every request
  • Deny by default โ€” only allow what’s explicitly permitted
  • Log access control failures and alert on repeated failures

2. Cryptographic Failures

Sensitive data exposed due to weak or missing encryption โ€” passwords in plaintext, unencrypted connections, weak algorithms.

# Bad: MD5 for passwords
import hashlib
hashed = hashlib.md5(password.encode()).hexdigest()

# Good: bcrypt with salt
import bcrypt
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())

Defenses:

  • Use HTTPS everywhere (TLS 1.2+)
  • Hash passwords with bcrypt, scrypt, or Argon2
  • Never store sensitive data you don’t need
  • Use strong, modern encryption (AES-256, RSA-2048+)

3. Injection (SQL, Command, LDAP)

Untrusted data is sent to an interpreter as part of a command or query.

# Bad: string concatenation in SQL
query = f"SELECT * FROM users WHERE email = '{email}'"

# Good: parameterized query
cursor.execute("SELECT * FROM users WHERE email = %s", (email,))
// Bad: shell command injection
const { exec } = require('child_process');
exec(`convert ${userInput} output.png`);

// Good: use arrays, never string interpolation
exec('convert', [sanitizedInput, 'output.png']);

Defenses:

  • Always use parameterized queries / prepared statements
  • Validate and sanitize all input
  • Use an ORM that handles escaping
  • Apply least-privilege to database accounts

4. Insecure Design

Security flaws baked into the architecture โ€” not just implementation bugs but missing threat modeling and secure design patterns.

Defenses:

  • Threat model during design phase
  • Use security design patterns (defense in depth, fail securely)
  • Limit data exposure โ€” only return what the client needs

5. Security Misconfiguration

Default credentials, unnecessary features enabled, verbose error messages, missing security headers.

# Bad: exposes server version
Server: nginx/1.18.0

# Good: hide version info
server_tokens off;

Defenses:

  • Disable default accounts and change default passwords
  • Remove unused features, components, and documentation
  • Review and harden all configuration files
  • Use automated scanning tools (e.g., nikto, lynis)

6. Vulnerable and Outdated Components

Using libraries, frameworks, or OS components with known vulnerabilities.

# Check for known vulnerabilities in Node.js dependencies
npm audit

# Python
pip-audit

# Ruby
bundle audit

Defenses:

  • Keep dependencies up to date
  • Subscribe to security advisories for your stack
  • Use tools like Dependabot, Snyk, or OWASP Dependency-Check

7. Identification and Authentication Failures

Weak passwords, missing MFA, session fixation, credential stuffing.

# Bad: no rate limiting on login
@app.route('/login', methods=['POST'])
def login():
    check_credentials(request.form['email'], request.form['password'])

# Good: rate limiting + lockout
@app.route('/login', methods=['POST'])
@limiter.limit("5 per minute")
def login():
    check_credentials(request.form['email'], request.form['password'])

Defenses:

  • Enforce strong password policies
  • Implement multi-factor authentication (MFA)
  • Rate-limit and lock out after failed attempts
  • Use secure, random session IDs; invalidate on logout

8. Software and Data Integrity Failures

Insecure deserialization, untrusted CI/CD pipelines, auto-updates without integrity checks.

# Bad: deserializing untrusted data with pickle
import pickle
data = pickle.loads(user_supplied_bytes)  # arbitrary code execution risk

# Good: use safe formats like JSON
import json
data = json.loads(user_supplied_string)

Defenses:

  • Verify integrity of downloaded packages (checksums, signatures)
  • Use signed commits and verified CI/CD pipelines
  • Avoid deserializing data from untrusted sources

9. Security Logging and Monitoring Failures

Attacks go undetected because logging is insufficient or logs are not monitored.

import logging

logger = logging.getLogger(__name__)

def login(email, password):
    user = find_user(email)
    if not user or not verify_password(password, user.password_hash):
        logger.warning("Failed login attempt for email=%s ip=%s", email, get_client_ip())
        return None
    logger.info("Successful login user_id=%s", user.id)
    return user

Defenses:

  • Log all authentication events, access control failures, and input validation errors
  • Include enough context (timestamp, user, IP, action)
  • Set up alerts for suspicious patterns
  • Store logs in a tamper-resistant location

10. Server-Side Request Forgery (SSRF)

The server fetches a URL supplied by the attacker, potentially reaching internal services.

# Bad: fetching user-supplied URL without validation
import requests

@app.route('/fetch')
def fetch():
    url = request.args.get('url')
    return requests.get(url).text  # attacker can hit internal services

# Good: allowlist permitted domains
ALLOWED_DOMAINS = {'api.example.com', 'cdn.example.com'}

def fetch_safe(url):
    from urllib.parse import urlparse
    parsed = urlparse(url)
    if parsed.hostname not in ALLOWED_DOMAINS:
        raise ValueError("Domain not allowed")
    return requests.get(url).text

Security Headers

HTTP response headers are a quick win for hardening your application:

# Content Security Policy โ€” restrict resource origins
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-{random}'" always;

# Prevent clickjacking
add_header X-Frame-Options "DENY" always;

# Prevent MIME sniffing
add_header X-Content-Type-Options "nosniff" always;

# Force HTTPS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

# Control referrer information
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Permissions policy
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

Check your headers at securityheaders.com.

Cross-Site Scripting (XSS)

XSS lets attackers inject scripts into pages viewed by other users.

// Bad: inserting raw user input into the DOM
document.getElementById('output').innerHTML = userInput;

// Good: use textContent (no HTML parsing)
document.getElementById('output').textContent = userInput;

// Good: escape HTML before inserting
function escapeHtml(str) {
  return str
    .replace(/&/g, '&')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#039;');
}

Cross-Site Request Forgery (CSRF)

CSRF tricks authenticated users into submitting requests they didn’t intend.

<!-- Include a CSRF token in every state-changing form -->
<form method="POST" action="/transfer">
  <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
  <input type="number" name="amount">
  <button type="submit">Transfer</button>
</form>
# Validate the token server-side
def transfer():
    if request.form['csrf_token'] != session['csrf_token']:
        abort(403)
    # proceed with transfer

Modern frameworks (Rails, Django, Laravel) handle CSRF protection automatically โ€” make sure it’s enabled.

Quick Security Checklist

  • HTTPS enforced everywhere
  • Passwords hashed with bcrypt/Argon2
  • All SQL queries parameterized
  • Input validated and sanitized
  • Security headers configured
  • Dependencies audited and up to date
  • Authentication rate-limited
  • Sensitive data not logged
  • Error messages don’t leak stack traces to users
  • CSRF protection on all state-changing endpoints

Resources

Comments