אחד מהדברים החשובים לאבטחה – במיוחד כשמיישמים פלאגינים או קוד של צד שלישי זה SRI – ראשי תבות של SubResource Integrity. כתבתי מבוא ל-SRI לפני כמה שנים והגיע הזמן קצת לחדש אותו ולהתאים אותו למציאות הפרונט אנדית הקיימת.
בגדול – אנחנו משתמשים, וזה מוסבר במבוא – ב- SRI – לוודא חתימה של סקריפטים ומהימנות שלהם. שזה נשמע מפוצץ אבל לא מובן. אז בואו ונסביר את זה עם התקפה שהיתה באמת – למשל האתר של Nagich. מדובר בשירות שמספק תוסף נגישות שאתרים משתמשים בו. התוסף נטען באמצעות קובץ ג׳אווהסקריפט שנמצא באתר של Nagich ונטען כתוסף צד שלישי. מה התוקפים עשו? שינו (לא משנה איך) את קובץ הג׳אווהסקריפט למשהו זדוני וכל האתרים שהשתמשו בו בעצם נדבקו בבת אחת. אפשר לקרוא פה את פרטי המקרה. איך מונעים את זה? אני מורה לאתר/אפליקציה שלי לטעון רק את המשאב הזה. SRI הוא מאד חשוב למשאבים חיצוניים/צד שלישי.
אבל אבל אבל במציאות של CDNים מרובים ואפליקציות שיושבות בכל מיני מקומות ומקבלות משאבים במקומות גם בסקריפטים שכביכול בשליטה שלי יכולה להיות בעיה: תוקף יכול גם להשפיע על הסקריפטים שלי – למשל אם הוא פורץ ל-CDN זה או אחר ואז נכנס לסקריפט מסוים ולהכניס לתוכו קוד זדוני שעושה שנינגנס.
⚠️ אם זה נשמע לכם תיאורטי, אז דלגו בעליזות לגוגל הקרוב לביתכם וחפשו British airways magecart fines וצפו בתדהמה על מתקפה מוצלחת שלא רק שהביאה לנזק אדיר לחברה – בעולם ה-GDPR של היום היא הביאה לקנסות עצומים של עשרות ומאות מיליונים.
איך אני מונע מתוקף להכנס לסקריפטים שמאוחסנים אצלי ולהכניס קוד זדוני? בכל הנוגע לסקריפטים שנטענים עם תגית סקריפט ו-src – אז יש לנו את SRI. זה מונע התקפה של השרת סקריפט זדוני שמתרחשת כאשר אני עושה דיפלוימנט לאפליקציה לאיזה ענן. עם ה-SRI אני יכול לוודא שקבצי הג׳אווהסקריפט שאני טוען, אפילו מה-CDN שלי הם באמת הקובץ שייצרתי בתהליך הבילד ולמנוע טעינה שלו אם יש שינוי כלשהו.
דוגמת ונילה
אז בואו ונראה איך זה עובד באתר ונילה פשוט.
אני אצור שני קבצים – קובץ אחד בשם index.html וקובץ שני בשם example-script.js.
אם אני רוצה להגן על הקובץ של example-script.js באמצעות SRI, אז זה קל. אני מחשב את ה-hash של הקובץ וממיר אותו ל-base64 באמצעות הפקודה:
cat example-script.js | openssl sha256 -binary | openssl base64
את התוצאה המתקבלת אני מציב בתכונת attribute בסקריפט. הנה דוגמה:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sample Page</title>
<script
integrity="sha256-eTVYEWRUIBm1AXrE88saXQsAeEyKRYueH28o4jXJ2nU="
src="example-script.js"
></script>
</head>
<body>
<h1>Hello, welcome to my webpage!</h1>
</body>
</html>
אני אריץ עם פקודת npx serve (לא יודע אם אתם מכירים את serve – אבל הוא מודול ממש כיפי של Node.js שמרים שרת אד הוק בכל תיקיה שאנחנו רוצים ונוח ממש לבדיקות קטנות). אני אראה שהכל עובד.
א-ב-ל אם אשנה את הקובץ עכשיו, ולו גם רווח, אני אקבל שגיאה בטעינה. וככה מונעים התקפות שונות ומשונות ותוספות. טכניקה שכדאי להכיר.
בגלל שזו טכניקה פשוטה, עושים אותה יחסית בקלות בלא מעט מקומות ותשתיות.
דוגמה עם Vite
בואו ונדגים עם אפליקציה סטטית של vite.js יקח אפליקצית Vite ונילה לגמרי בלי לגעת בכלום ונריץ את npm run build. ייבנו לנו קבצים סטטיים בתיקית ה-build שלנו. אפשר לבחון אותם באמצעות כניסה ל-dist/assets – נראה שאחד מהם הוא קובץ ג׳אווהסקריפט.
על מנת לבחון את האפליקציה באוויר נריץ את npm run preview. נוכל לראות את האפליקציה שרצה מקבצי הבילד. אחד מהם הוא קובץ ג׳אווהסקריפט:
זה קובץ שנטען אוטומטית והכל מעולה. אבל אם תוקף ישנה אותו, הדפדפן יטען אותו כרגיל מבלי להסס ויוזרק קוד זדוני. כדי להורות לדפדפן לטעון רק את הקובץ הזה, אני אחשב לו hash. איך אני עושה את זה? עם הפקודה הבאה:
cat dist/assets/index-25780642.js | openssl dgst -sha256 -binary | openssl base64
הפקודה תביא לי תוצאה של hash שמקודד ל-base64. במקרה הזה והייחודי שלי:
sha256-r4P8yKzvh2Yk0N7ZKjWC8CMF2gbIIPimoJyT3U6+96M=
אחרי שנריץ את הבילד- נלך לאפליקציה הסטטית תחת תיקית dist נכנס לדף ה-index.html ונוסיף את הערך ל-intergrity של הסקריפט. זה ייראה בערך ככה:
<script type="module" integrity="sha256-r4P8yKzvh2Yk0N7ZKjWC8CMF2gbIIPimoJyT3U6+96M=" crossorigin src="/assets/index-25780642.js"></script>
אם אני אריץ את האפליקציה באמצעות:
npm run preview
האפליקציה תעלה בלי בעיות.
עכשיו נשחק אותה תוקפים. נפתח את dist/assets/index-25780642.js עם העורך שלי ואוסיף שורה של console.log כלשהי, או אפילו סתם רווח, אשמור וארפרש, אני אראה שאין טעינה.
דוגמה מציאותית עם Vite
כמובן שזו דוגמת hello world. אם אנחנו רוצים כזה דבר אבל לא רוצים לחשב כל פעם את ה-hash או אפילו לכתוב סקריפט שעושה את זה – יש ל-Vite פלגינים שבונים SRI אוטומטית. בגדול הם עושים עם Node.js בדיוק את הפעולה הזו – לאחר הבילד הם מריצים סקריפט שמחשב את ה-hash ומכניס אותו ל-HTML המג׳ונרט. זה הכל.
כדי להדגים נשתמש בפלגין הזה של Vite.js. הוא ישן ולא מעודכן, אבל הקוד שלו קטן והוא משתמש במודולי ליבה בעיקר (למעט שני מודולים נוספים) – אני הייתי משתמש בו או מעתיק פשוט את הקוד שלו. נתקין את הקוד שלו כ-dev dependency פה:
npm i --save-dev @small-tech/vite-plugin-sri
ב-vite.config.js (אם אין לכם, פשוט צרו אותו ב-root) הוסיפו את הפלגין כך:
import { defineConfig } from 'vite'
import sri from '@small-tech/vite-plugin-sri'
export default defineConfig({
plugins: [sri()]
})
וזהו, זה יעבוד.
זה כמובן רלוונטי לכל קובץ שהוא – גם קובץ CSS. אני רק אזכיר שיש CSS Injections 🙂 זוכרים את עירית רחובות?
מה עם תשתיות אחרות? תשתיות סטטיות כוללות בד״כ אינטגרציה מובנית עם SRI בדיוק כמו VIte. אבל אם מדובר ברינדור בצד שרת כמו ב-Next.js נצטרך להשתמש בפלגין של וובפאק שנקרא webpack-subresource-integrity. אבל הוא קשה לתפירה והאמת היא שאיפה שאין אפליקציה סטטית אני אעדיף להשתמש ב-nonce. אכתוב על כך במאמר אחר.
כמובן שלא בטוח שכדאי לכם להשתמש ב-SRI. זה תלוי בכם וב-usecase שלכם. זה כלי שכדאי להכיר.
או קיי, אז הבנו איך עובדים עם SRI בקבצים חיצוניים עם Vite. מה קורה עם קובץ inline? על זה במאמר הבא: על hash ב-inline.