693 lines
20 KiB
JavaScript
693 lines
20 KiB
JavaScript
/**
|
|
* TikTok Privacy and Network Security (PNS) Runtime - Deobfuscated
|
|
* Original file: index.js
|
|
*
|
|
* This is TikTok's Privacy and Network Security (PNS) system that:
|
|
* - Controls web API usage and ensures compliance with privacy policies
|
|
* - Monitors and intercepts network requests for security
|
|
* - Implements cookie consent management
|
|
* - Provides comprehensive analytics and tracking
|
|
* - Manages service worker communication
|
|
* - Enforces content security policies
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
/**
|
|
* Core Constants and Event Types
|
|
*/
|
|
const EVENT_TYPES = {
|
|
MAIN_THREAD: "main_thread",
|
|
OUT_APP: "out_app",
|
|
COOKIE_SET_BY_DOCUMENT: "cookie_set_by_document",
|
|
COOKIE_BLOCKED_ON_START: "cookie_blocked_on_start",
|
|
GENERAL_FETCH: "general_fetch",
|
|
REQUEST_LOG: "request_log",
|
|
WEBAPI: "webapi",
|
|
STORAGE_USE: "storage_use",
|
|
SW_INCOMPAT: "sw_incompat",
|
|
READY_FOR_MSG: "ready_for_msg",
|
|
FORCE_UPDATE_SW: "force_update_sw",
|
|
FREQUENCY: "frequency",
|
|
COST_TIME: "cost_time",
|
|
MAIN_THREAD_CTX: "main_thread_ctx",
|
|
NETWORK_RULE: "network_rule"
|
|
};
|
|
|
|
const SW_EVENTS = {
|
|
RUNTIME_SW_EVENT: "__PNS_RUNTIME_SW_EVENT__",
|
|
RUNTIME_SE_ERROR: "__PNS_RUNTIME_SE_ERROR__",
|
|
RUNTIME: "__PNS_RUNTIME__"
|
|
};
|
|
|
|
/**
|
|
* Global Runtime Instance Manager
|
|
* Creates and manages the global PNS runtime instance
|
|
*/
|
|
function createGlobalRuntime(globalName = getGlobalName()) {
|
|
let runtime = globalThis[globalName];
|
|
|
|
if (!runtime) {
|
|
runtime = {
|
|
pendingEvents: [],
|
|
pendingConfig: {},
|
|
pendingListeners: {},
|
|
errors: [],
|
|
|
|
/**
|
|
* Push event to pending queue
|
|
*/
|
|
pushEvent: function(eventName, eventDetail = null, source = EVENT_TYPES.MAIN_THREAD, options) {
|
|
addToQueue(runtime.pendingEvents, {
|
|
eventName,
|
|
eventDetail,
|
|
source,
|
|
options
|
|
}, 100);
|
|
},
|
|
|
|
/**
|
|
* Push error to error queue
|
|
*/
|
|
pushError: function(error) {
|
|
addToQueue(runtime.errors, error, 20);
|
|
},
|
|
|
|
pageContextObservers: []
|
|
};
|
|
|
|
globalThis[globalName] = runtime;
|
|
}
|
|
|
|
return runtime;
|
|
}
|
|
|
|
/**
|
|
* Get global name from script parameters or use default
|
|
*/
|
|
function getGlobalName() {
|
|
const scriptSrc = document.currentScript?.src;
|
|
try {
|
|
const url = new URL(scriptSrc);
|
|
return url.searchParams.get("globalName") || SW_EVENTS.RUNTIME;
|
|
} catch (error) {
|
|
return SW_EVENTS.RUNTIME;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add item to queue with size limit
|
|
*/
|
|
function addToQueue(queue, item, maxSize) {
|
|
queue.splice(0, queue.length - maxSize + 1);
|
|
queue.push(item);
|
|
}
|
|
|
|
/**
|
|
* Privacy and Network Security Core Classes
|
|
*/
|
|
|
|
/**
|
|
* Cookie Consent Manager
|
|
* Handles cookie blocking and consent management
|
|
*/
|
|
class CookieConsentManager {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.blockedCookies = this.getBlockedCookies(config.blockers);
|
|
}
|
|
|
|
/**
|
|
* Get list of cookies to block based on domain matching
|
|
*/
|
|
getBlockedCookies(blockers = []) {
|
|
for (const blocker of blockers) {
|
|
const { domains = [], cookies = [] } = blocker;
|
|
if (domains.some(domain => location.hostname.endsWith(domain))) {
|
|
return cookies;
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Hook document.cookie setter to intercept cookie operations
|
|
*/
|
|
hookCookieSetter(reportCallback) {
|
|
const originalDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie');
|
|
|
|
Object.defineProperty(document, 'cookie', {
|
|
set: (value) => {
|
|
const cookieData = this.processCookieSet(value, reportCallback);
|
|
|
|
if (!cookieData._blocked) {
|
|
originalDescriptor.set.call(document, value);
|
|
}
|
|
|
|
return cookieData._blocked;
|
|
},
|
|
get: originalDescriptor.get,
|
|
configurable: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Process cookie setting with privacy rules
|
|
*/
|
|
processCookieSet(cookieValue, reportCallback) {
|
|
const cookieData = {
|
|
rawValue: cookieValue,
|
|
name: this.getCookieName(cookieValue),
|
|
_time: Date.now(),
|
|
_blocked: false,
|
|
_sample_rate: this.config.sampleRate,
|
|
_stack_rate: 0,
|
|
_rule_names: []
|
|
};
|
|
|
|
// Check if cookie should be blocked
|
|
cookieData._blocked = this.blockedCookies.includes(cookieData.name);
|
|
|
|
// Apply privacy rules and report if needed
|
|
if (Math.random() < cookieData._sample_rate) {
|
|
reportCallback(cookieData);
|
|
}
|
|
|
|
return cookieData;
|
|
}
|
|
|
|
getCookieName(cookieString) {
|
|
const name = cookieString.split('=')[0];
|
|
return name ? name.trim() : undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Network Request Interceptor
|
|
* Intercepts and processes all network requests for security and privacy
|
|
*/
|
|
class NetworkInterceptor {
|
|
constructor(config, callbacks) {
|
|
this.config = config;
|
|
this.callbacks = callbacks;
|
|
this.originalFetch = window.fetch;
|
|
this.originalXHR = window.XMLHttpRequest;
|
|
|
|
this.hookFetch();
|
|
this.hookXMLHttpRequest();
|
|
}
|
|
|
|
/**
|
|
* Hook fetch API for request interception
|
|
*/
|
|
hookFetch() {
|
|
const self = this;
|
|
|
|
window.fetch = function(...args) {
|
|
const request = new Request(...args);
|
|
const requestData = self.extractRequestData(request);
|
|
|
|
// Apply security rules
|
|
const processedData = self.applySecurityRules(requestData);
|
|
|
|
if (processedData._blocked) {
|
|
return Promise.resolve(new Response("Request blocked", {
|
|
status: 410,
|
|
statusText: "Request blocked by privacy policy"
|
|
}));
|
|
}
|
|
|
|
// Report request for analytics
|
|
self.callbacks.report(processedData);
|
|
|
|
return self.originalFetch.apply(this, args);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook XMLHttpRequest for legacy request interception
|
|
*/
|
|
hookXMLHttpRequest() {
|
|
const self = this;
|
|
const OriginalXHR = this.originalXHR;
|
|
|
|
window.XMLHttpRequest = class extends OriginalXHR {
|
|
constructor() {
|
|
super();
|
|
this.__pumbaa_detail = {};
|
|
}
|
|
|
|
open(method, url, ...args) {
|
|
this.__pumbaa_detail = {
|
|
method: method.toUpperCase(),
|
|
request_url: new URL(url, location.href).href,
|
|
_request_time: Date.now(),
|
|
_blocked: false
|
|
};
|
|
|
|
return super.open(method, url, ...args);
|
|
}
|
|
|
|
send(body) {
|
|
const processedData = self.applySecurityRules(this.__pumbaa_detail);
|
|
|
|
if (processedData._blocked) {
|
|
this.status = 410;
|
|
this.statusText = "Request blocked";
|
|
return;
|
|
}
|
|
|
|
self.callbacks.report(processedData);
|
|
return super.send(body);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract request data for processing
|
|
*/
|
|
extractRequestData(request) {
|
|
const url = new URL(request.url);
|
|
|
|
return {
|
|
request_url: request.url,
|
|
request_host: url.host,
|
|
request_path: url.pathname,
|
|
search: url.search,
|
|
method: request.method,
|
|
headers: this.extractHeaders(request.headers),
|
|
_request_time: Date.now(),
|
|
_blocked: false,
|
|
_sample_rate: 0
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract headers from request
|
|
*/
|
|
extractHeaders(headers) {
|
|
const headerMap = {};
|
|
headers.forEach((value, key) => {
|
|
headerMap[key] = value;
|
|
});
|
|
return headerMap;
|
|
}
|
|
|
|
/**
|
|
* Apply security and privacy rules to requests
|
|
*/
|
|
applySecurityRules(requestData) {
|
|
// Check against blocklist/allowlist
|
|
if (this.isRequestBlocked(requestData.request_url)) {
|
|
requestData._blocked = true;
|
|
requestData["x-pns-block"] = "1";
|
|
}
|
|
|
|
// Apply URL replacement rules
|
|
const modifiedUrl = this.applyUrlReplacement(requestData.request_url);
|
|
if (modifiedUrl !== requestData.request_url) {
|
|
requestData.request_url = modifiedUrl;
|
|
requestData["x-pns-replace"] = "1";
|
|
requestData._replaced_fields = ["url"];
|
|
}
|
|
|
|
return requestData;
|
|
}
|
|
|
|
/**
|
|
* Check if request should be blocked
|
|
*/
|
|
isRequestBlocked(url) {
|
|
const { blocklist = [], allowlist = [] } = this.config;
|
|
|
|
// Check blocklist
|
|
if (blocklist.some(blocked => url.startsWith(blocked))) {
|
|
return true;
|
|
}
|
|
|
|
// Check allowlist (if exists, URL must be in it)
|
|
if (allowlist.length > 0) {
|
|
return !allowlist.some(allowed => url.startsWith(allowed));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Apply URL replacement rules (e.g., HTTP to HTTPS)
|
|
*/
|
|
applyUrlReplacement(url) {
|
|
const { replace = {} } = this.config;
|
|
|
|
for (const [pattern, replacement] of Object.entries(replace)) {
|
|
if (url.startsWith(pattern)) {
|
|
return replacement + url.substring(pattern.length);
|
|
}
|
|
}
|
|
|
|
return url;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Web API Monitor
|
|
* Monitors usage of sensitive web APIs
|
|
*/
|
|
class WebAPIMonitor {
|
|
constructor(config, reportCallback) {
|
|
this.config = config;
|
|
this.reportCallback = reportCallback;
|
|
this.hookSensitiveAPIs();
|
|
}
|
|
|
|
/**
|
|
* Hook sensitive web APIs for monitoring
|
|
*/
|
|
hookSensitiveAPIs() {
|
|
const apis = this.config.apis || [];
|
|
|
|
apis.forEach(api => {
|
|
this.hookAPI(api);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hook individual API
|
|
*/
|
|
hookAPI(apiConfig) {
|
|
const { apiObj, apiName, apiType, sampleRate, block } = apiConfig;
|
|
const target = this.getAPITarget(apiObj);
|
|
|
|
if (!target) return;
|
|
|
|
const originalMethod = target[apiName];
|
|
if (typeof originalMethod !== 'function') return;
|
|
|
|
const self = this;
|
|
|
|
target[apiName] = function(...args) {
|
|
// Report API usage
|
|
if (Math.random() < sampleRate) {
|
|
self.reportCallback({
|
|
apiRule: apiConfig,
|
|
args: args,
|
|
_blocked: block
|
|
});
|
|
}
|
|
|
|
// Block if configured
|
|
if (block) {
|
|
console.warn(`API ${apiName} blocked by privacy policy`);
|
|
return;
|
|
}
|
|
|
|
return originalMethod.apply(this, args);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get API target object
|
|
*/
|
|
getAPITarget(apiObj) {
|
|
if (!apiObj) return window;
|
|
|
|
const parts = apiObj.split('.');
|
|
let target = window;
|
|
|
|
for (const part of parts) {
|
|
target = target[part];
|
|
if (!target) return null;
|
|
}
|
|
|
|
return target;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Page Context Manager
|
|
* Manages page context and navigation tracking
|
|
*/
|
|
class PageContextManager {
|
|
constructor() {
|
|
this.observers = [];
|
|
this.context = this.buildInitialContext();
|
|
this.setupNavigationTracking();
|
|
}
|
|
|
|
/**
|
|
* Build initial page context
|
|
*/
|
|
buildInitialContext() {
|
|
const url = new URL(location.href);
|
|
|
|
return {
|
|
url: url.href,
|
|
host: url.host,
|
|
path: url.pathname,
|
|
search: url.search,
|
|
hash: url.hash,
|
|
region: this.getRegion(),
|
|
business: this.getBusiness(),
|
|
env: this.getEnvironment()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Setup navigation change tracking
|
|
*/
|
|
setupNavigationTracking() {
|
|
// Track popstate events
|
|
window.addEventListener('popstate', () => {
|
|
this.updateContext(this.buildInitialContext());
|
|
});
|
|
|
|
// Hook history API
|
|
['pushState', 'replaceState'].forEach(method => {
|
|
const original = History.prototype[method];
|
|
History.prototype[method] = function(...args) {
|
|
original.apply(this, args);
|
|
// Update context after navigation
|
|
setTimeout(() => {
|
|
this.updateContext(this.buildInitialContext());
|
|
}, 0);
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update page context and notify observers
|
|
*/
|
|
updateContext(newContext) {
|
|
const changes = this.getContextChanges(this.context, newContext);
|
|
|
|
if (Object.keys(changes).length > 0) {
|
|
Object.assign(this.context, changes);
|
|
|
|
// Notify observers
|
|
this.observers.forEach(observer => {
|
|
if (!observer.fields || observer.fields.some(field => field in changes)) {
|
|
observer.func(this.context);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get context changes
|
|
*/
|
|
getContextChanges(oldContext, newContext) {
|
|
const changes = {};
|
|
|
|
for (const key in newContext) {
|
|
if (oldContext[key] !== newContext[key]) {
|
|
changes[key] = newContext[key];
|
|
}
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
/**
|
|
* Add context observer
|
|
*/
|
|
addObserver(callback, fields) {
|
|
this.observers.push({ func: callback, fields });
|
|
}
|
|
|
|
getRegion() {
|
|
// Extract region from meta tags or config
|
|
return document.querySelector('meta[name="pumbaa-ctx"]')?.content?.region || 'unknown';
|
|
}
|
|
|
|
getBusiness() {
|
|
// Extract business context
|
|
return document.querySelector('meta[name="pumbaa-web-config"]')?.content?.business || 'tiktok';
|
|
}
|
|
|
|
getEnvironment() {
|
|
// Determine environment (prod, staging, dev)
|
|
return location.hostname.includes('tiktok.com') ? 'prod' : 'dev';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main PNS Runtime Class
|
|
* Orchestrates all privacy and security components
|
|
*/
|
|
class PNSRuntime {
|
|
constructor() {
|
|
this.config = this.loadConfiguration();
|
|
this.pageContext = new PageContextManager();
|
|
this.setupComponents();
|
|
}
|
|
|
|
/**
|
|
* Load PNS configuration from embedded script or meta tags
|
|
*/
|
|
loadConfiguration() {
|
|
// Try to load from embedded script tag
|
|
const configScript = document.getElementById('pumbaa-rule');
|
|
if (configScript) {
|
|
try {
|
|
return JSON.parse(configScript.textContent);
|
|
} catch (error) {
|
|
console.warn('Failed to parse PNS config:', error);
|
|
}
|
|
}
|
|
|
|
// Fallback to default configuration
|
|
return this.getDefaultConfig();
|
|
}
|
|
|
|
/**
|
|
* Setup all PNS components
|
|
*/
|
|
setupComponents() {
|
|
const runtime = createGlobalRuntime();
|
|
|
|
// Setup cookie consent management
|
|
if (this.config.cookie?.enabled) {
|
|
const cookieManager = new CookieConsentManager(this.config.cookie);
|
|
cookieManager.hookCookieSetter((data) => {
|
|
runtime.pushEvent(EVENT_TYPES.COOKIE_SET_BY_DOCUMENT, data);
|
|
});
|
|
}
|
|
|
|
// Setup network interception
|
|
if (this.config.network?.enabled) {
|
|
const networkInterceptor = new NetworkInterceptor(this.config.network, {
|
|
report: (data) => {
|
|
runtime.pushEvent(EVENT_TYPES.GENERAL_FETCH, data);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Setup web API monitoring
|
|
if (this.config.webapi?.enabled) {
|
|
const apiMonitor = new WebAPIMonitor(this.config.webapi, (data) => {
|
|
runtime.pushEvent(EVENT_TYPES.WEBAPI, data);
|
|
});
|
|
}
|
|
|
|
// Setup service worker communication
|
|
this.setupServiceWorkerCommunication(runtime);
|
|
}
|
|
|
|
/**
|
|
* Setup service worker communication for enhanced security
|
|
*/
|
|
setupServiceWorkerCommunication(runtime) {
|
|
if ('serviceWorker' in navigator) {
|
|
navigator.serviceWorker.addEventListener('message', (event) => {
|
|
if (event.data.event === SW_EVENTS.RUNTIME_SW_EVENT) {
|
|
// Handle service worker events
|
|
runtime.pushEvent(event.data.eventName, event.data.data);
|
|
}
|
|
});
|
|
|
|
navigator.serviceWorker.ready.then((registration) => {
|
|
const sw = registration.active || navigator.serviceWorker.controller;
|
|
if (sw) {
|
|
// Send configuration to service worker
|
|
sw.postMessage({
|
|
eventName: EVENT_TYPES.READY_FOR_MSG,
|
|
source: EVENT_TYPES.MAIN_THREAD,
|
|
data: this.pageContext.context
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get default configuration if none provided
|
|
*/
|
|
getDefaultConfig() {
|
|
return {
|
|
cookie: {
|
|
enabled: true,
|
|
sampleRate: 0.07,
|
|
blockers: []
|
|
},
|
|
network: {
|
|
enabled: true,
|
|
sampleRate: 0.03,
|
|
intercept: []
|
|
},
|
|
webapi: {
|
|
enabled: true,
|
|
apis: []
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize PNS Runtime
|
|
* Main entry point that starts the privacy and security system
|
|
*/
|
|
function initializePNS() {
|
|
// Check if already initialized
|
|
if (window.__PUMBAA_RUN_FLAG__) {
|
|
return;
|
|
}
|
|
|
|
window.__PUMBAA_RUN_FLAG__ = true;
|
|
|
|
try {
|
|
// Initialize the PNS runtime
|
|
const pnsRuntime = new PNSRuntime();
|
|
|
|
console.log('TikTok Privacy and Network Security (PNS) initialized');
|
|
|
|
// Load core PNS module
|
|
const script = document.createElement('script');
|
|
script.src = './core.js?globalName=' + getGlobalName();
|
|
script.crossOrigin = 'anonymous';
|
|
script.async = true;
|
|
|
|
// Copy dataset from current script
|
|
if (document.currentScript) {
|
|
Object.assign(script.dataset, document.currentScript.dataset);
|
|
}
|
|
|
|
document.head.appendChild(script);
|
|
document.head.removeChild(script);
|
|
|
|
} catch (error) {
|
|
console.error('Failed to initialize PNS:', error);
|
|
|
|
// Report error to global runtime
|
|
const runtime = createGlobalRuntime();
|
|
runtime.pushError(error);
|
|
}
|
|
}
|
|
|
|
// Auto-initialize if conditions are met
|
|
if (typeof window !== 'undefined' &&
|
|
window.Symbol &&
|
|
!(/ByteLocale/g.test(navigator.userAgent))) {
|
|
|
|
initializePNS();
|
|
}
|