יישום של nonce על מנת להגן מפני התקפות injection

בפוסט הקודם הסברתי על hash עם CSP על משאבי inline – שזה נחמד ומעולה אבל פחות ישים בעולם האמיתי שבו בדרך כלל התוכן ה-inline (בין אם מדובר בג׳אווהסקריפט או בין אם מדובר ב-CSS) מיוצר כל הזמן על ידי ריאקט, CSS-in-JS וחבריהם.

אם מדובר באתרים דינמיים, אפשר וצריך להכניס nonce כחלק ממדיניות CSP על מנת להפעיל הגנה מפני הזרקה של סקריפטים. מהות ההגנה היא בעצם חתימה של הסקריפט בכל פעם שהוא יוצא מהשרת (בגלל זה צריך שרת דינמי) והוכחה שהוא באמת סקריפט שיצא מהשרת.

על מנת להמחיש את ההתקפה, בואו ונניח אתר עם חולשת XSS. חולשה די פשוטה ומוכרת – באתר אפשר לחפש ומילות החיפוש נשלחות כ: http://example.com/search?q=QUERY ומודפסות באופן הזה:

<!DOCTYPE html>
<html>
<head>
    <title>Search Page</title>
</head>
<body>
    <!-- Example of a valid script with the nonce -->
    <script>
        // Valid script here
    </script>

    <div>Your search results for: <?= $_GET['q'] ?></div>
</body>
</html>

תוקף מרושע יכול לעשות משהו כזה:

http://example.com/search?q=<script>alert('some xss shit')</script>

אז איך אני מונע כזה דבר? אני יכול לאסור ב-CSP על inline. אבל זה ידפוק את ה-valid script שיכול להכיל בתוכו מן הגורן ומן היקב. אני יכול לעשות hash כפי שהסברתי בפוסט הקודם אבל יש גם אפשרות אחרת – לשים לסקריפטים שאני בוחר nonce. את אותו nonce אני מבקש מהשרת לשלוח ב-CSP ואותו אני מצמיד רק לסקריפטים שאני מציב. באופן הבא:

<!DOCTYPE html>
<html>
<head>
    <title>Search Page</title>
</head>
<body>
    <!-- Example of a valid script with the nonce -->
    <script nonce="rAnd0m">
        // Valid script here
    </script>

    <div>Your search results for: <?= $_GET['q'] ?></div>
</body>
</html>

אם ב-CSP יהיה את ה-nonce, שבמקרה הזה הוא rAnd0m, אז הסקריפט התקין ייטען אבל שום דבר אחר לא.

בואו ונדגים. הנה דוגמה ל-CSP שאני מכניס בתגית מטא:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <meta
      http-equiv="Content-Security-Policy"
      content="script-src 'self' 'nonce-rAnd0m'"
    />
  </head>
  <body>
    <h1 id="header">Hello, World!</h1>
    <script nonce="rAnd0m">
      document.getElementById("header").textContent = "DOM Manipulated!";
    </script>
  </body>
</html>

אם אני אכניס סקריפט אחר – הוא לא ייטען ואני אקבל שגיאת CSP.

מה שחשוב הוא לא לבחור ערך nonce קבוע אלא כזה שמשתנה בכל קריאה כדי שהתוקף לא יכול לנחש אותו. בשביל זה גם צריך מחולל רנדומלי קריפטוגרפי (כתבתי על זה במאמר הזה).

כמובן ש-nonce אפשר להציב אך ורק באתרים דינמיים. כזה שיש שרת מאחוריהם ולא קבצי ג׳אווהסקריפט שזרוקים באיזה S3 מאחורי קלאודפרונט זה או אחר. ובגלל שמדובר בטכניקת הגנה פשוטה, יש לא מעט דוגמאות. למשל הדוגמה הזו בדוקומנטציה של next שקל מאד להפעיל וזה עובד הישר מהקופסה. יוצרים middleware.ts ומציבים אותו בתיקיה הראשית – src במקרה של next 14.

import { NextRequest, NextResponse } from 'next/server'
 
export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const cspHeader = `
    script-src 'self' 'nonce-${nonce}' 'strict-dynamic';
`
  // Replace newline characters and spaces
  const contentSecurityPolicyHeaderValue = cspHeader
    .replace(/\s{2,}/g, ' ')
    .trim()
 
  const requestHeaders = new Headers(request.headers)
  requestHeaders.set('x-nonce', nonce)
 
  requestHeaders.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue
  )
 
  const response = NextResponse.next({
    request: {
      headers: requestHeaders,
    },
  })
  response.headers.set(
    'Content-Security-Policy',
    contentSecurityPolicyHeaderValue
  )
 
  return response
}

ומה עם caching? אין בעיה להחזיק פה cache, כי הדפדפן לא מוודא שה-nonce הזה הוא ייחודי ולא סופק בעבר/לאחר. זה כבר כאב הראש שלכם לוודא שה-nonce הוא ייחודי או לפחות לא משתכפל לכמה אנשים שונים ושתוקף לא יוכל לנחש אותו.

לסיכום – כדאי בהחלט להכיר, במיוחד אם דורשים מכם ליישם CSP.

פוסטים נוספים שכדאי לקרוא

תמונת תצוגה של מנעול על מחשב
פתרונות ומאמרים על פיתוח אינטרנט

הגנה מפני XSS עם Trusted Types

תכונה ב-CSP שמאפשרת מניעה כמעט הרמטית להתקפות XSS שכל מפתח ווב צריך להכיר וכדאי שיכיר.

גלילה לראש העמוד