לא מזמן נתקלתי במקרה אבטחה מרגיז ועצוב במיוחד, במסגרת המקרה, הבנתי שיש פערים טכניים בהבנה איך אפשר לחסום הורדת משאבים ללא בקרת שליטה באתר שמחויב לחשוף מידע. אז הנה, באתי להסביר כמה דרכים לעשות כן.
אז הנה המקרה: יש לנו אתר המנגיש מידע באופן פומבי – בין אם כתמונות, , קבצי PDF או קבצים אחרים או בעצם כל משאב שהוא שנמצא אצלנו.אנחנו כן רוצים לאפשר הורדה דרך האתר, אבל לא באמצעים אחרים. למה? כדי למנוע למשל הורדה מקישורים ישירים מגוגל או למנוע מאתרים אחרים להציב קישורים לנכסים היקרים שלי. או כדי למנוע כרייה או ניתוח של הנתונים.
שלוש הערות בעקבות התגובות: הראשונה בעקבות התגובה של מרק וירין היא שצריך לשים לב שמדובר בתסריט שבו אין לנו access control, שזו הדרך הטובה ביותר למנוע הורדות לא מורשות אבל לא תמיד אפשר לנקוט בה (למשל אתר המנגיש מידע ממשלתי). המטרה היא להקשות, לא למנוע.
השניה, בעקבות ההערה של אדם טל, היא שניתן ליצור קישור זמני לפרק זמן קצר או לשימוש חד פעמי ואני אפרט על כך במאמר נפרד ואיך עושים את זה ב-AWS.
השלישית, בעקבות ההערה של לירן טל: הקוד פה הוא דוגמה בלבד. אל תעתיקו אותו לשרתי הפרודקשן שלכם (אל תעתיקו שום קוד ישירות לפרודקשן אגב).
למשל – אם יש חשש שהחומר שנמצא אצלי מכיל פרטים אישיים, אני לא רוצה שגוגל יתחילו לסרוק אותו ולהציף אותו ברשת בחיפוש פשוט. אם יש לי כמות גדולה של מסמכים כאלו, אני רוצה למנוע כרייה מסיבית – ובקיצור: אני רוצה שרק מי שיגיע מהאתר שלי יוכל להוריד את המשאבים שלי. יש כמה אסטרטגיות לעשות כן ונעבור על כולן. אפשר כמובן לשלב חלק מהן או את כולן.
בדיקה של referrer
זו שיטה מאד אפקטיבית ופשוטה ליישום ומעולה למניעת hotlinking (שיטה נלוזה שהיתה מאד נפוצה בסוף שנות ה-90/תחילת ה-2000 ובה אתרי אינטרנט השתמשו ב-src ממקור אחר כדי לחסוך בכסף של אחסון). אני אדגים אותה עם Node.js ןאקספרס די קלאסי למשל:
const express = require('express');
const app = express();
const PORT = 3000;
app.use((req, res, next) => {
const allowedReferrer = 'https://your-allowed-referrer.com';
// Check the referer header
if (req.headers.referer === allowedReferrer) {
next(); // Proceed if the referrer matches
} else {
res.status(403).send('Access Denied: Invalid Referrer');
}
});
app.get('/protected-resource', (req, res) => {
res.send('This is a protected resource');
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
הקוד הוא די straight forward ושמתי אותו באמת רק להראות כמה זה פשוט – לא ממש צריך להבין Node.js על מנת להבין איך זה עובד וכמובן שניתן לממש את זה בכל שפה שהיא. אגב, לא חייבים לעשות את זה בכלל אפליקטיבי (כלומר בקוד) אלא בתשתית. בד״כ בשרתי אפשר לעשות את זה בהגדרות, למשל ב-Apache (כן, כן) אפשר לעשות את זה עם apache2.conf או עם .htaccess. הנה דוגמה שג׳ינטרתי מהר:
RewriteEngine on
RewriteCond %{HTTP_REFERER} !^http://(www\.)?your-allowed-domain\.com [NC]
RewriteCond %{HTTP_REFERER} !^http://(www\.)?your-allowed-domain\.com.*$ [NC]
RewriteCond %{HTTP_REFERER} !^https://(www\.)?your-allowed-domain\.com [NC]
RewriteCond %{HTTP_REFERER} !^https://(www\.)?your-allowed-domain\.com.*$ [NC]
RewriteCond %{HTTP_REFERER} !^$
RewriteRule \.(jpg|jpeg|png|gif|zip|pdf)$ - [F]
אפשר לעשות את זה בכל שרת, nginx, אפילו בטומקאט (עם פילטרים). מי שרוצה לדעת יותר על htaccess יכול לקרוא פה.
מה הבעיה עם הדרך הזו? שלמרות שקל ליישם אותה, היא לא מאד אפקטיבית. כלומר אפקטיבית למי שינסה להוריד דרך גוגל אבל לא למי שרוצה לעקוף כי קל מאד לזייף את ההדר של referrer או למחוק אותו. אבל איך עיברי לידר כתב: ״יותר טוב כמעט מכלום״. (או שהוא כתב ההיפך? לא משנה).
לא לתת לגוגל לסרוק את האתר
דרך נוספת היא להכניס הנחיה לגוגל ב-robots.txt לא לסרוק את הקבצים – זו דרך שכדאי לנקוט בה כדי לא להציף או להציג את הקבצים היקרים שלי בפני אלו שעושים גוגל דורקינג – כלומר משתמשים בגוגל על מנת למצוא קבצים מעניינים. כאן למשל אני מורה לגוגל, בינג ושאר החברים שלא לסרוק את תיקית files אצלי בשם המתחם ולא להציג שום דבר הקשור אליה.
User-agent: *
Disallow: /files/
השיטה הזו תמנע מהקבצים שלי להופיע בגוגל, אבל לא תמנע מאנשים להכנס עצמאית לאתר ולהוריד אותם. למי שלא מכיר את robots.txt – מומלץ להכיר ולו רק מסיבות אבטחה.
עוגיות
הגישה השלישית היא מספיק טובה על מנת למנוע ממשתמשים שלא ביקרו באתר להוריד את הקבצים. היא מאד לא הרמטית אבל לפחות היא מבטיחה שמשתמשים שביקרו באתר בתקופת זמן מסוימת יוכלו להוריד את הקבצים וזהו. גם זו אפשרות שיחסית קל לבצע ברוב אפליקציות הווב הקיימות. בגדול – יוצרים עוגיה למי שנכנס לאתר עם תוקף מסוים ובמהלך ההורדה של הקבצים בודקים שיש לו את העוגיה הזו. למתקדמים ניתו להכניס איזשהו מפתח על מנת לוודא שהעוגיה נוצרה על ידי השרת. שימו לב שזה לא access control. כל אחד יכול לקבל את העוגיה.
מה שחשוב הוא לוודא שהעוגיה נוצרת עם שלושה פרמטרים:
- httpOnly – שרק לצד שרת תהיה גישה אליה, על מנת למנוע התקפות ושנינגנס כללי.
- secure – שרק ב-https תהיה גישה אליה.
- sameSite: 'Strict' – שהעוגיה לא תעבור בבקשות אלו ואחרות.
על מנת להדגים כמה זה קל – אני אראה שוב עם Node.js ואחר כך גם עם פייתון. מי שלא מכיר את שתי השפות וכן רוצה להבין – שינסה פשוט להתייחס אליהן כפסאודו קוד:
const express = require('express');
const session = require('express-session');
const app = express();
const port = 3000;
// Middleware to create a session cookie
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
secure: true,
sameSite: 'strict'
}
}));
// Middleware to protect /api/folder
app.use('/api/folder', (req, res, next) => {
if (!req.session.cookieId) {
req.session.cookieId = req.sessionID;
}
if (!req.sessionID) {
return res.status(403).send('Access denied');
}
next();
});
// Serve files from the /api/folder directory
app.use(express.static('/api/folder'));
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
והנה דוגמה קטנה בפייתון עם fastapi ו-uvicorn. שימו לב שזו דוגמה שרק מראה כמה זה קל, לא משהו שהייתי מדביק בפרודקשן כי יש כאן כמה ענייני אבטחה שהתעלמתי מהן באופן אלגנטי.
from fastapi import FastAPI, Request, Response, HTTPException, Depends
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import FileResponse
import os
app = FastAPI()
# Configure session middleware
app.add_middleware(SessionMiddleware, secret_key="your-secret-key")
# Dependency to verify session
def verify_session(request: Request):
if not request.session.get('session_id'):
request.session['session_id'] = "session-{}".format(os.urandom(12).hex())
return
if 'session_id' not in request.session:
raise HTTPException(status_code=403, detail="Access denied")
// Pay attention: This is not a secured code and the file_path should be sanitized.
@app.get("/api/folder/{file_path:path}")
async def get_file(file_path: str, request: Request, session_verified: None = Depends(verify_session)):
file_location = f"/api/folder/{file_path}"
if not os.path.exists(file_location):
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(file_location)
אם אתם משתמשים בשפה אחרת, באמת אין מה להתבאס כי זה תסריט כל כך נפוץ שיש לו מענה בכמעט כל פריימוורק שאני מכיר. כמובן שלא הכנסתי כאן לוגיקה מתקדמת של בדיקת זמנים או בדיקה כמה קבצים אותו לקוח הוריד אבל בהחלט אפשרי לעשות כן.
עטיפה של ההורדה בלוגיקה
דרך מעט יותר מסובכת ליישום באתר קיים אבל אפקטיבית מאד היא עטיפת כל הורדה בלוגיקה עסקית עם הגנה שמוודאת שרק מי שלחץ על קישור מתוך האתר מקבל את הקובץ. זה כמובן גם מאט כרייה (ועם הגדרות WAF מתאימות יכול לעצור אותה לחלוטין או לפחות להקשות עליה מאד). זה מסובך רק בגלל שצריך לעטוף כל הורדה בפונקציה שתקרא ל-route מסוים שרק דרכו אפשר להוריד את הקבצים ולא לאפשר הורדה חופשית לעולם. אבל ברוב הפריימוורקים יש הגנת CSRF מובנית, בדיוק כמו ה-session.
הנה דוגמה ב-Node.js שמייצרת גם קליינט. סביר להניח שבאפליקציה אמיתית זה יהיה מסובך יותר אבל בגדול העקרון הוא אותו עקרון. השרת מייצר CSRF token, ושם אותו בצד הלקוח, כשיש הורדה, צד הלקוח שולח את הטוקן והשרת מוודא שזה אותו טוקן חד פעמי. זהו. אפשר ליישם את זה בכמה דרכים.
const express = require('express');
const session = require('express-session');
const csrf = require('csurf');
const path = require('path');
const app = express();
const port = 3000;
// Session configuration
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: true
}));
// Enable CSRF protection
const csrfProtection = csrf({ cookie: false });
app.use(express.static('public'));
// Serve the download page with CSRF token
app.get('/download', csrfProtection, (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Download Page</title>
</head>
<body>
<h1>Download Page</h1>
<p>Click the button below to download the file.</p>
<form action="/api/Attachment" method="GET">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<button type="submit">Download</button>
</form>
</body>
</html>
`);
});
// Pay attention: this is just a demo, do not use unsanitzed input
// Download route with CSRF validation
app.get('/api/Attachment', csrfProtection, (req, res) => {
res.download(path.join(__dirname, 'path-to-your-file'));
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
עם Node.js וגם פייתון הדברים האלו קלים יחסית כי יש תשתיות מוכנות, אבל גם בשפות ובפריימוורקים אחרים יש (למשל יש ב-ASP.net core) כי כאמור זו בעיה טריוויאלית שמתרחשת בהרבה מקומות. וכשיש בעיה טריוויאלית, יש לה פתרונות טריוויאליים. אם אתם מנסים לפתור בעיה טריוויאלית באופן לא טריוויאלי או לחלופין – טוענים שאין פתרון – אתם בבעיה.
כמובן שיש עוד דרכים לפתור את הבעיה הזו וייתכן שהדרכים שלעיל לא מושלמות. יש כמה דרכים לממש את הפתרון. אם מישהו או מישהי רוצים לחלוק את הדרך שלהם עבדה – זה יהיה נחמד 🙂
14 תגובות
כפתור הורדה שיוצר Signed URL עם תוקף קצר שמספיק להורדת הקובץ ולא יותר.
זה מה שרוב התעשייה עושה (לפחות עם S3) וזה עושה את העבודה בצורה הבטוחה ביותר.
נכון! ואפשר ליישם את זה לא עם s3 ולא כתבתי את זה. אולי אני אעדכן את המאמר.
האמת שבמחשבה שניה זה מאד AWS oriented אז אני אכתוב על זה פוסט נפרד. תודה על הרעיון!
שלילי חזק. צריך להיות ברור לכל אחד שכל מה שעולה לאתר ונגיש לאנשים שאין להם מוטיבציה לשמור על חיסיון התוכן הוא נגיש לכולם וזו רק שאלה של זמן. כל הטריקים המפורטים פה הם בסך הכל דרכים לבעל האתר לשקר לעצמו ולהעמיד פנים שהתוכן שלו חסוי.
אם אתה לא מוכן להשקיע בניהול משתמשים אמיתי עם זיהוי סביר שאכן ב"עולם האמיתי" היית חולק עימם את התוכן, כל מה שמוצג פה הוא בזבוז זמן שפוטנציאלית יכול לייצר בעיות שאתה לא מצפה להן בזמנים שאין לך חשק לנתח ולהתמודד איתן
…. ןלא, robots.txt לא ימנע מגיגול להציג קישור לתוכן שלך, שלא לדבר על זה שיש מנועי חיפוש שלא מסתכלים בכלל מה יש בקובץ הזה.
אני מסכים שהדרך הכי טובה היא access control, אבל לפעמים יש צורך במתן אפשרות להוריד רק מהאתר. האם משתמש זדוני יכול להוריד את הכל ולחשוף את זה בנכס משלו? כן, אבל בכמויות גדולות של מידע זה לא ישים.
רן, אני חושב שכדאי לציין disclaimer בתחילת המאמר שמדגיש שאם המידע נגיש באינטרנט ללא הרשאות באופן קבוע (ולא, למשל, ל-5 דקות) אז המידע שמור בצורה לא מאובטחת וכל השיטות המוצגות פה נועדו להקשות על גורמים לא מורשים ולא באמת יכולות לעצור אותם.
צודק. כמובן שהמטרה היא להקשות כמה שיותר על גורמים לא מוקשים שלא נכנסים דרך האתר.
מה זה שנינגנס?
ציטוט: " על מנת למנוע התקפות ושנינגנס כללי."
כל השאר סבבה חחח
שננינגנס זה תעלולים 🙂
*שנניגנס (shenanigans)
מה אפשר לעשות בוורדפרס כדי אני בונה אתרין בוורדפרס, לפעמים יש צורך בהעלאת קובץ על ידי הגולש.
התוסף שאני משתמשת איתו הוא jet (מאד פופולרי)
הוא לא שולח את הקובץ במייל אלא מאחסן אותו בתיקיית uploads ושולח לינק לצפייה/הורדה.
איך אני יכולה למנוע גישה לתיקיית uploads?
חסמתי כמובן את הסריקה של גוגל לתיקייה הזאת, אבל זה לא מונע "ניחוש" של הurl והורדת קבצים.
אשמח לעצה
בגלל שזו מערכת כל כך פופולרית, יש כמה שיטות ודרכים שמבוססים בגדול על מה שיש פה – שינוי htaccess ועטיפה בפונקציה. הנה דוגמה לדיון כזה.
תודה, אבדוק