SOURDOUGH FOR SERVICENOW Security Review Document — v1.0 Full version at: https://thesnowball.co/security ──────────────────────────────────────────────── SUMMARY ──────── Sourdough is a Chrome extension (Manifest V3) that adds a monitoring and debugging dashboard to ServiceNow. All API calls are read-only GET requests made from the user's browser directly to their own ServiceNow instance. No instance data is transmitted to any Sourdough-controlled server. ──────────────────────────────────────────────── 1. CHROME PERMISSIONS (exact from manifest.json) ──────────────────────────────────────────────── "permissions": ["activeTab", "storage", "tabs"] "host_permissions": ["https://extensionpay.com/*"] NOTE: service-now.com is NOT in host_permissions. The extension accesses ServiceNow via the user's existing browser session (credentials: "include") and the activeTab permission for the current tab only. "content_scripts": [ { "matches": [""], "js": ["content-script.js"], "run_at": "document_end" }, { "matches": ["https://extensionpay.com/*"], "js": ["ExtPay.js"], "run_at": "document_start" } ] IMPORTANT NOTE ON : The content script's sole function is to listen for a message from the popup requesting the ServiceNow g_ck token. It reads window.g_ck and returns it via postMessage. On non-ServiceNow pages, window.g_ck is undefined and nothing is returned. No page content is read on non-ServiceNow domains. NOT REQUESTED (absent from manifest): - webRequest - declarativeNetRequest - identity (no OAuth) - management - history - bookmarks - cookies - downloads - nativeMessaging ──────────────────────────────────────────────── 2. AUTHENTICATION FLOW (from content-script.js and gck-accessor.js) ──────────────────────────────────────────────── Step 1: User opens the Sourdough popup. Step 2: Popup sends chrome.tabs.sendMessage(tab.id, { action: 'getGckToken' }) to the active tab's content script. Step 3: Content script injects gck-accessor.js into the page context via a script tag (CSP-compliant approach). Step 4: gck-accessor.js reads window.g_ck and posts back: window.postMessage({ type: 'SOURDOUGH_GCK_RESPONSE', g_ck: window.g_ck || null, url: window.location.href }, '*'); Step 5: Popup receives the token and uses it as X-UserToken header. Timeout: 3000ms if token not available. The g_ck token: - Is ServiceNow's own CSRF token used by ServiceNow's UI for all API calls - Is session-scoped (expires when session ends) - Is stored in MEMORY ONLY during the popup session - Is NEVER written to chrome.storage, localStorage, or any database ──────────────────────────────────────────────── 3. EXACT FETCH SIGNATURE (from servicenow.js) ──────────────────────────────────────────────── const headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'X-UserToken': g_ck // in-memory CSRF token }; fetch(url, { method: 'GET', // ALL calls are GET — no POST/PUT/PATCH/DELETE headers, credentials: 'include' // browser session cookies }); Authentication validation (stats.js): GET /api/now/table/incident?sysparm_limit=1&sysparm_fields=sys_id,number Headers: { 'X-UserToken': g_ck } Purpose: Confirm active session before loading dashboard ──────────────────────────────────────────────── 4. SERVICENOW API ENDPOINTS (complete list from servicenow.js) ──────────────────────────────────────────────── All calls: GET method, credentials: "include", results limited via sysparm_limit (100-500 records per call). Endpoint Purpose ────────────────────────────────────────────────────── /api/now/table/incident Incidents by priority, state, group /api/now/table/change_request Change requests by state /api/now/table/task Assigned tasks, SLA data /api/now/table/task_sla SLA stage breakdown /api/now/table/sys_outbound_http_log Outbound HTTP errors and call volume /api/now/table/ecc_queue ECC/MID server queue /api/now/table/ecc_agent MID server health /api/now/table/sysapproval_approver Approval queue /api/now/table/cmdb_ci CMDB CIs /api/now/table/cmdb_ci_computer Server/computer records /api/now/table/sys_properties Version detection (glide.war value only) /stats.do Instance health page (read-only HTML) /replication.do Replication status (read-only) All calls respect the user's ACLs. A 403 results in "no access" displayed in the panel — no data is returned or stored. ──────────────────────────────────────────────── 5. EXTERNAL CONNECTIONS ──────────────────────────────────────────────── extensionpay.com (payment processor): - Manages free trial and paid subscription state - Background worker runs: extpay.startBackground() - Origin validation enforced: events from origins other than 'https://extensionpay.com' are rejected - Data stored: installation timestamp, trial start date, subscription paid date - NO ServiceNow data is involved or transmitted Chrome Web Store (Google-managed): - Standard automatic update mechanism - No Sourdough-controlled server in the update path NO analytics, telemetry, crash reporting, or user behavior tracking. No ServiceNow instance data flows to any external server. ──────────────────────────────────────────────── 6. DATA STORAGE ──────────────────────────────────────────────── chrome.storage.sync (synced across user's Chrome profiles): Key: "instances" - Array of {name, url} for saved instances Key: "currentInstance" - Currently selected {name, url} NO instance data, credentials, or session tokens stored here. localStorage (browser, per-device, auto-expires): Prefix: frontdoor_cache_* - Instance health metrics: 3-hour TTL - HTTP error log data: 3-hour TTL - System log data: 1-hour TTL - Version info: 24-hour TTL - Dark mode preference: Persistent (UI setting only) Cache cleared on instance switch and via manual UI button. Memory only (never persisted): - g_ck authentication token (cleared when popup closes) ExtensionPay (chrome.storage.local, managed by ExtPay library): Key: extensionpay_installed_at - Installation timestamp Key: extensionpay_user - Subscription/trial status object Key: extensionpay_api_key - ExtensionPay API identifier ──────────────────────────────────────────────── 7. BACKGROUND SERVICE WORKER (background.js) ──────────────────────────────────────────────── The entire background service worker is 3 lines of functional code: importScripts('ExtPay.js'); const extpay = ExtPay('sourdough'); extpay.startBackground(); Purpose: Initialize ExtensionPay to handle subscription webhooks. No API calls to ServiceNow are made from the background worker. ──────────────────────────────────────────────── 8. ENTERPRISE DEPLOYMENT OPTIONS ──────────────────────────────────────────────── Extension ID: bbalpiojmggfbkjlnldlkmmailaakpbh Options via Google Admin Console: a) Force-install: ExtensionInstallForcelist b) Allowlist: ExtensionInstallAllowlist c) Block all except allowlisted: add ID to per-OU allowlist d) Version pinning: supported via Chrome Enterprise policy Distribution: Chrome Web Store only. All updates pass Google review. ──────────────────────────────────────────────── 9. CONTACT ──────────────────────────────────────────────── Security questions: hello@thesnowball.co Feedback and issues: https://thesnowball.co/feedback Full security page: https://thesnowball.co/security Happy to provide additional documentation, answer specific questions from your security team, or get on a call. ──────────────────────────────────────────────── Sourdough is an independent Chrome extension. Not affiliated with, endorsed by, or sponsored by ServiceNow, Inc. Document current as of April 2026. ────────────────────────────────────────────────