Content Security Policy (CSP) היא כותרת HTTP שהשרת שולח עם כל עמוד, ובה רשימה: מאיפה מותר לטעון סקריפטים, סגנונות, תמונות, גופנים ומסגרות. דפדפן שמקבל אותה מסרב לטעון כל דבר שלא ברשימה, כולל סקריפט שתוקף הצליח להזריק דרך תגובה או שדה טופס. זו אחת ההגנות היעילות נגד XSS, וגם אחת הדרכים הקלות ביותר להשבית אתר בטעות.
הכלל הראשון: לא לאכוף ביום הראשון
יש שתי כותרות. Content-Security-Policy אוכפת. Content-Security-Policy-Report-Only לא חוסמת כלום, רק שולחת דיווח על כל מה שהיה נחסם. מתחילים תמיד מהשנייה. היא מגלה לכם כל סקריפט, תמונה ו-iframe שלא זכרתם, בלי שאף גולש ייפגע.
כך נראית מדיניות ראשונה, מחמירה בכוונה:
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' data:; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://your-site.co.il/csp-report"
'self' פירושו ״מהדומיין של העמוד בלבד״. הכותרת Reporting-Endpoints מגדירה לאן ישלחו הדיווחים; ההנחיה הישנה report-uri מסומנת ב-MDN כמיושנת, אך עדיין מומלץ לציין את שתיהן עד שכל הדפדפנים תומכים ב-report-to.
הכלל השני: לקרוא את הדיווחים
הדפדפן שולח JSON עם הדומיין שנחסם, ההנחיה שהופרה, והעמוד. צריך משהו שיקבל אותם: סקריפט קטן בשרת ששומר לקובץ, או שירות ייעודי. אחרי שבוע של תנועה אמיתית תהיה לכם רשימה של כל מה שהאתר באמת טוען. חלק מהדיווחים יהיו רעש, בעיקר מתוספי דפדפן של גולשים, שמזריקים סקריפטים משלהם; לומדים לזהות אותם לפי דומיינים שלא קשורים אליכם.
הבעיה האמיתית: סקריפטים inline
רוב האתרים הקיימים כוללים JavaScript בתוך ה-HTML: <script> עם קוד, onclick בתגים, קוד שתוסף הדביק. מדיניות 'self' חוסמת את כולם, כי אין להם ״מקור״. הפתרון הגרוע הוא 'unsafe-inline', שמבטל את רוב ההגנה. שני פתרונות טובים:
- nonce: השרת מייצר בכל תגובה מחרוזת אקראית, מצרף אותה לכותרת (
script-src 'nonce-R4nd0m') ולכל תג סקריפט לגיטימי (<script nonce="R4nd0m">). תוקף שמזריק סקריפט לא יודע את ה-nonce של התגובה הזאת. חשוב: ערך חדש בכל תגובה, לא קבוע. - hash: לסקריפט שלא משתנה, מציינים את ה-SHA-256 של תוכנו. מתאים לקוד קטן וקבוע, למשל הסקריפט שמחיל התאמות נגישות לפני הציור הראשון.
ולסקריפטים שטוענים סקריפטים אחרים (מנהלי תגים, ספריות), 'strict-dynamic' אומר לדפדפן: סקריפט שאושר ב-nonce או ב-hash רשאי לטעון עוד סקריפטים, גם בלי nonce. זה מה שמאפשר מדיניות קפדנית בלי לתחזק רשימת דומיינים אינסופית.
סדר עבודה שעובד
- הוסיפו Report-Only עם
default-src 'self'ואיסוף דיווחים. חכו שבוע. - עברו על הדיווחים. לכל מקור חיצוני החליטו: להוסיף לרשימה, להעביר לאחסון שלכם, או להסיר.
- הוסיפו nonce לסקריפטים שלכם. בתבנית PHP זה אומר: לייצר ערך פעם אחת בתחילת הבקשה, ולהדפיס אותו בכותרת ובכל תג.
- הדקו:
script-src 'nonce-…' 'strict-dynamic'; object-src 'none'; base-uri 'self'. שתי האחרונות סוגרות דרכי עקיפה ותיקות. - כשהדיווחים שקטים במשך שבוע, שנו את שם הכותרת ל-
Content-Security-Policy. השאירו את Report-Only במקביל עם מדיניות מחמירה יותר, כדי להתכונן לשלב הבא.
מה CSP לא פותרת
היא לא מונעת הזרקה; היא מונעת מההזרקה לרוץ. סינון קלט ובריחת פלט (escaping) נשארים חובה. היא גם לא עוזרת נגד סקריפט לגיטימי שנפרץ בצד הספק שלו: אם ספריית צד שלישי שאישרתם משתנה, הדפדפן יריץ אותה. לזה נועד integrity (SRI) על תגי סקריפט חיצוניים, ועדיף פשוט לארח את הספריות אצלכם.
ומילה על אתר על אחסון משותף: את הכותרות אפשר לשלוח גם מ-.htaccess (Header set Content-Security-Policy-Report-Only "…"), אבל nonce דורש קוד שרץ בכל בקשה, ולכן הוא שייך ל-PHP ולא לקובץ הגדרות סטטי.