Is Substital: Add subtitles to videos and movies safe?
Adds searchable or user-provided subtitles to videos on streaming websites.
This extension detects videos on sites such as Netflix, HBO Max, Disney+, Amazon Prime Video, YouTube, and other video platforms after the user opens it. Users can search for subtitles, drag and drop an SRT or ZIP file, adjust synchronization, and change subtitle styling such as font size, text color, and background color.
AI-generated. Findings may contain errors. Those marked Verified have been manually reviewed.
Publishers can request a review.
Findings
Bundled donation SDK lets a third party remotely reconfigure the extension
Substital bundles a fundraising SDK from GiveFreely.
Via the extension's all-websites access, it fetches two live files from cdn.givefreely.com: a charity list and a popup/merchant/analytics config, changeable anytime with no new version.
- Severity
- Medium unwanted
- Type
- Unexpected
- CWE
- CWE-829
- Source
- Dynamic sandbox
You load any webpage with Substital installed.
The content script runs on every site because the extension holds <all_urls> host permissions.
The extension fetches live configuration files from a third party's server and uses them to decide whether to show a donation checkout popup on that page.
The same files control which merchants are excluded from the popup and where the SDK's own usage events are sent.
12,394-byte JSON of default English translation and charity-list strings, observed alongside a smaller partner-specific overlay at cdn.givefreely.com/adunit/language/substitalprod/en.json (2 bytes) that merges on top. Reproduced with identical response sizes across two independent sessions.
23,241-byte JSON containing the behavioral flags shown below, observed with a 2,150-byte partner override merged on top from cdn.givefreely.com/adunit/behavioral/substitalprod.json. Reproduced with identical response sizes across two independent sessions, ruling out a one-off cache artifact.
- Checkout popup switchcheckoutPopupMasterSwitch: false (illustrative — key confirmed present, live value not logged)
A single remote flag turns Substital's checkout popup on or off for every installed copy at once, with no extension update involved.
- Merchant exclusionsmerchantExclusions: [{ domain: "target.com", mutePopups: true }] (illustrative)
A list of merchant domains where the popup is suppressed. GiveFreely can add or remove merchants from this list at any time.
- Merchant popup rulesmerchantPopupRules: [{ domain: "walmart.com", rules: [{ rule: "/checkout/i" }] }] (illustrative)
Per-merchant regex rules that decide which checkout pages the popup activates on.
- Analytics event endpointeventsUrl: https://events.givefreely.com/v1/events (illustrative)
The URL the bundled SDK reports its own usage events to. This destination is also set by the remote config.
The remote config fetch and the switch that consumes it
Config service — fetches and merges the two remote files
382.background.jsconst j = "https://cdn.givefreely.com/adunit/behavioral/";class W { constructor(e) { this.partnerApiKey = e, this.partnerConfig = null, this.globalConfig = null, this.mergedConfig = null, this.globalConfigRefreshInterval = null, this.partnerConfigRefreshInterval = null, this.eventEmitter = new N, this.isInitialized = !1, this.partnerCircuitBreaker = new z({ failureThreshold: 3, recoveryTimeout: 6e4, monitoringWindow: 3e5, storageKey: `gf_partner_circuit_breaker_${e}` }), this.globalCircuitBreaker = new z({ failureThreshold: 3, recoveryTimeout: 6e4, monitoringWindow: 3e5, storageKey: "gf_global_circuit_breaker" }) } async initialize() { this.isInitialized || (await Promise.all([this.partnerCircuitBreaker.initialize(), this.globalCircuitBreaker.initialize()]), this.isInitialized = !0) } ensureInitialized() { if (!this.isInitialized) throw new Error("GiveFreelyConfigService must be initialized before use. Call initialize() or use create() method.") } getRetryOptions() { return { delayFn: e => 1e3 * e, maxAttempts: 3 } } async fetchPartnerConfig() { const e = await u((async () => { const e = await fetch(`${j}${this.partnerApiKey}.json`, { cache: "no-store" }); if (!e.ok) throw new Error(`Failed to fetch partner config: ${e.statusText}`); return e.json() }), this.getRetryOptions()); if (!e) throw new Error("Failed to fetch or get from cache partner config"); return this.partnerConfigRefreshInterval = e.configRefreshInterval || this.partnerConfigRefreshInterval, e } async fetchGlobalConfig() { const e = await u((async () => { const e = await fetch(`${j}global.json`, { cache: "no-store" }); if (!e.ok) throw new Error(`Failed to fetch global config => Status code: ${e.status} .${e.statusText}`); return e.json() }), this.getRetryOptions()); if (!e) throw new Error("Failed to fetch or get from cache global config", e); return this.globalConfigRefreshInterval = e.configRefreshInterval || this.globalConfigRefreshInterval, e } async getCachedConfig() { return this.mergedConfig } async getConfig(e = !1) { this.ensureInitialized(); let t = !1; e && (t = await U()); const [r, i] = await Promise.all([R.get(w, this.fetchPartnerConfig.bind(this), this.partnerConfigRefreshInterval, t, (e => e?.configRefreshInterval ?? null)), R.get(y, this.fetchGlobalConfig.bind(this), this.globalConfigRefreshInterval, t, (e => e?.configRefreshInterval ?? null))]); if (!i.content) throw new Error("Global config is required"); if (!r.content) throw new Error("Partner config is required"); this.partnerConfig = r.content, this.globalConfig = i.content; const s = this.mergeConfigs(JSON.parse(JSON.stringify(this.globalConfig)), JSON.parse(JSON.stringify(this.partnerConfig))); if (!s) throw new Error("Was not able to merge configs"); return ((e, t, r) => { const i = [...t.merchantExclusions ?? [], ...r.merchantExclusions ?? []]; e.merchantExclusions = i })(s, this.globalConfig, this.partnerConfig), this.emitConfigChanges(s), s }}Checkout popup gated on the remote switch
content-script.jsif (u(l || {}), e.logger.debug("Charities for current country", { currentCountry: a, charities: l }), !1 === e.popupConfig.checkoutPopupMasterSwitch) return void e.logger.debug("Checkout popup master switch is disabled");- cdn.givefreely.com
Serves the language/charity content and behavioral config (checkoutPopupMasterSwitch, merchantExclusions, merchantPopupRules, eventsUrl) governing the SDK's behavior on every page.
- events.givefreely.com
Receives the bundled SDK's own usage/analytics events, at a destination set by the same remote config.
What it can do
Permissions this extension asks for, as declared in version 2.10.3. Asking for a permission is not a finding on its own - it is what the extension can do if it chooses to. The current listed version is 2.10.7, which we have not unpacked yet.
Read and change your data on every site you visit
<all_urls>
Act on the current tab, but only after you click the extension
activeTab
Store data in your browser
storage
Run its own code inside the pages you visit
scripting
Watch every request your browser makes
webRequest