diff --git a/packages/adblocker/assets/update.js b/packages/adblocker/assets/update.js index 93294fbb98..756e292562 100644 --- a/packages/adblocker/assets/update.js +++ b/packages/adblocker/assets/update.js @@ -27,6 +27,7 @@ const FILTER_LISTS = [ async function downloadResource(resourceName) { const { revisions } = await fetch( `https://cdn.ghostery.com/adblocker/resources/${resourceName}/metadata.json`, + { redirect: 'error' }, ).then((result) => { if (!result.ok) { throw new Error( @@ -38,6 +39,7 @@ async function downloadResource(resourceName) { const latestRevision = revisions.at(-1); return fetch( `https://cdn.ghostery.com/adblocker/resources/${resourceName}/${latestRevision}/list.txt`, + { redirect: 'error' }, ).then((result) => { if (!result.ok) { throw new Error(`Failed to fetch ${resourceName}: ${result.status}: ${result.statusText}`); diff --git a/packages/adblocker/package.json b/packages/adblocker/package.json index 32bedaae1a..64ba2e7a1d 100644 --- a/packages/adblocker/package.json +++ b/packages/adblocker/package.json @@ -104,7 +104,6 @@ "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", "@types/node": "25.6.0", - "axios": "^1.15.2", "chai": "^6.2.2", "concurrently": "^9.2.1", "eslint": "^10.2.1", diff --git a/packages/adblocker/src/fetch.ts b/packages/adblocker/src/fetch.ts index 7e375dc30e..95a958d9a1 100644 --- a/packages/adblocker/src/fetch.ts +++ b/packages/adblocker/src/fetch.ts @@ -12,13 +12,21 @@ interface FetchResponse { json: () => Promise; } -export type Fetch = (url: string) => Promise; +interface FetchInit { + redirect?: 'error' | 'follow' | 'manual'; +} + +export type Fetch = (url: string, init?: FetchInit) => Promise; /** * Built-in fetch helpers can be used to initialize the adblocker from * pre-built presets or raw lists (fetched from multiple sources). In case of * failure (e.g. timeout), the whole process of initialization fails. Timeouts * are not so uncommon, and retrying to fetch usually succeeds. + * + * Redirects are rejected (`redirect: 'error'`) to protect against redirect-based + * attacks where a compromised or hijacked origin could point to an attacker- + * controlled host serving malicious filter lists. */ export function fetchWithRetry(fetch: Fetch, url: string): Promise { let retry = 3; @@ -28,7 +36,7 @@ export function fetchWithRetry(fetch: Fetch, url: string): Promise => { - return fetch(url).catch((ex) => { + return fetch(url, { redirect: 'error' }).catch((ex) => { if (retry > 0) { retry -= 1; return new Promise((resolve, reject) => { diff --git a/packages/adblocker/tools/stress-test-engine-update.ts b/packages/adblocker/tools/stress-test-engine-update.ts index 6e0518b505..083bb08e4c 100644 --- a/packages/adblocker/tools/stress-test-engine-update.ts +++ b/packages/adblocker/tools/stress-test-engine-update.ts @@ -19,7 +19,6 @@ * 5. check: no ID collisions for filters */ -import axios from 'axios'; import { brotliDecompressSync } from 'zlib'; import { Config, @@ -148,20 +147,22 @@ function filtersDiff( return differences; } +async function fetchOrThrow(url: string): Promise { + const response = await fetch(url, { redirect: 'error' }); + if (!response.ok) { + throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`); + } + return response; +} + async function getMeta(url: string): Promise<{ name: string; revisions: string[] }> { - const meta = (await axios.get(url)).data; - if (typeof meta === 'string') { - const buffer = Buffer.from( - ( - await axios.get(url, { - responseType: 'arraybuffer', - }) - ).data, - ); + const text = await (await fetchOrThrow(url)).text(); + try { + return JSON.parse(text); + } catch { + const buffer = Buffer.from(await (await fetchOrThrow(url)).arrayBuffer()); return JSON.parse(brotliDecompressSync(buffer).toString('utf-8')); } - - return meta; } /** @@ -184,15 +185,9 @@ async function getRevision(url: string): Promise { return cached; } - let data: string = (await axios.get(url)).data; + let data = await (await fetchOrThrow(url)).text(); if (!data.startsWith('[Ad')) { - const buffer = Buffer.from( - ( - await axios.get(url, { - responseType: 'arraybuffer', - }) - ).data, - ); + const buffer = Buffer.from(await (await fetchOrThrow(url)).arrayBuffer()); try { data = brotliDecompressSync(buffer).toString('utf-8'); diff --git a/yarn.lock b/yarn.lock index 9185836db9..f2cd19aae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -996,7 +996,6 @@ __metadata: "@types/chai": "npm:^5.2.3" "@types/mocha": "npm:^10.0.10" "@types/node": "npm:25.6.0" - axios: "npm:^1.15.2" chai: "npm:^6.2.2" concurrently: "npm:^9.2.1" eslint: "npm:^10.2.1" @@ -3517,7 +3516,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.12.0, axios@npm:^1.15.2": +"axios@npm:^1.12.0": version: 1.15.2 resolution: "axios@npm:1.15.2" dependencies: