@paywalls-net/filter 1.3.13 → 1.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -185,7 +185,18 @@ async function proxyVAIRequest(cfg, request) {
|
|
|
185
185
|
cf.country ? `co=${cf.country}, re=${cf.region || ''}, ci=${cf.city || ''}, asn=${cf.asn || ''}` : null);
|
|
186
186
|
|
|
187
187
|
// Tier 3: UA features + HMAC (§6.3)
|
|
188
|
-
|
|
188
|
+
let uaFeatures = extractUAFeatures(headers['user-agent']);
|
|
189
|
+
|
|
190
|
+
// Sec-Fetch context mismatch detection (paywalls-site-dz23):
|
|
191
|
+
// vai.json is fetched via sync XHR — browsers set Sec-Fetch-Dest: empty,
|
|
192
|
+
// Mode: cors. If we see document/navigate, the headless browser is leaking
|
|
193
|
+
// page-level Sec-Fetch onto the XHR. Emit marker so cloud-api can classify.
|
|
194
|
+
if (uaFeatures && cloudApiPath.endsWith('/vai.json') &&
|
|
195
|
+
headers['sec-fetch-dest'] === 'document' && headers['sec-fetch-mode'] === 'navigate') {
|
|
196
|
+
uaFeatures += ', sec-fetch-mismatch';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
setIfPresent(forwardHeaders, 'X-PW-UA', uaFeatures);
|
|
189
200
|
setIfPresent(forwardHeaders, 'X-PW-UA-HMAC', await computeUAHMAC(headers['user-agent'], cfg.vaiUAHmacKey));
|
|
190
201
|
setIfPresent(forwardHeaders, 'X-PW-CT-FP', await computeConfidenceToken(headers['user-agent'], headers['accept-language'], headers['sec-ch-ua']));
|
|
191
202
|
|
package/src/signal-extraction.js
CHANGED
|
@@ -514,6 +514,11 @@ export function extractUAFeatures(userAgent) {
|
|
|
514
514
|
if (AUTOMATION_MARKERS.some(re => re.test(ua))) parts.push('automation');
|
|
515
515
|
if (isFabricatedVersion(ua)) parts.push('fabricated');
|
|
516
516
|
|
|
517
|
+
// Stale version: Chrome family with ver=0-79 is 6+ years old (pre-2020)
|
|
518
|
+
if (family === 'chrome' && extractMajorVersion(ua) !== null && extractMajorVersion(ua) < 80) {
|
|
519
|
+
parts.push('stale');
|
|
520
|
+
}
|
|
521
|
+
|
|
517
522
|
parts.push(`entropy=${computeUAEntropy(ua)}`);
|
|
518
523
|
|
|
519
524
|
return parts.join(', ');
|
|
@@ -200,5 +200,64 @@ describe('Legitimate browser UAs — correct feature extraction', () => {
|
|
|
200
200
|
expect(result).toMatch(/ver=120-139/);
|
|
201
201
|
expect(result).not.toMatch(/headless/);
|
|
202
202
|
expect(result).not.toMatch(/fabricated/);
|
|
203
|
+
expect(result).not.toMatch(/stale/);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ── 7. Stale browser version marker ───────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
describe('Stale browser version — signal extraction', () => {
|
|
210
|
+
test('Chrome/59 (2017, ver=0-79) should have stale marker', () => {
|
|
211
|
+
const result = extractUAFeatures(
|
|
212
|
+
'Mozilla/5.0 (Linux; Android 7.0; SM-G930V Build/NRD90M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.125 Mobile Safari/537.36'
|
|
213
|
+
);
|
|
214
|
+
expect(result).toMatch(/ver=0-79/);
|
|
215
|
+
expect(result).toMatch(/\bstale\b/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('Chrome/79 (2019, ver=0-79) should have stale marker', () => {
|
|
219
|
+
const result = extractUAFeatures(
|
|
220
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.79 Safari/537.36'
|
|
221
|
+
);
|
|
222
|
+
expect(result).toMatch(/ver=0-79/);
|
|
223
|
+
expect(result).toMatch(/\bstale\b/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('Chrome/83 (80-99 bucket, borderline old) should NOT have stale marker', () => {
|
|
227
|
+
const result = extractUAFeatures(
|
|
228
|
+
'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
|
|
229
|
+
);
|
|
230
|
+
expect(result).toMatch(/ver=80-99/);
|
|
231
|
+
expect(result).not.toMatch(/\bstale\b/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test('Chrome/145 (current) should NOT have stale marker', () => {
|
|
235
|
+
const result = extractUAFeatures(
|
|
236
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36'
|
|
237
|
+
);
|
|
238
|
+
expect(result).toMatch(/ver=140-159/);
|
|
239
|
+
expect(result).not.toMatch(/\bstale\b/);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test('Safari/17 should NOT have stale marker', () => {
|
|
243
|
+
const result = extractUAFeatures(
|
|
244
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_4_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4.1 Safari/605.1.15'
|
|
245
|
+
);
|
|
246
|
+
expect(result).not.toMatch(/\bstale\b/);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('Firefox/130 should NOT have stale marker', () => {
|
|
250
|
+
const result = extractUAFeatures(
|
|
251
|
+
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0'
|
|
252
|
+
);
|
|
253
|
+
expect(result).not.toMatch(/\bstale\b/);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('Fabricated Chrome/48.0.1025.1402 also gets stale (both markers)', () => {
|
|
257
|
+
const result = extractUAFeatures(
|
|
258
|
+
'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.1025.1402 Mobile Safari/537.36'
|
|
259
|
+
);
|
|
260
|
+
expect(result).toMatch(/\bfabricated\b/);
|
|
261
|
+
expect(result).toMatch(/\bstale\b/);
|
|
203
262
|
});
|
|
204
263
|
});
|
|
@@ -291,6 +291,56 @@ describe('proxyVAIRequest — signal extraction pipeline', () => {
|
|
|
291
291
|
});
|
|
292
292
|
});
|
|
293
293
|
|
|
294
|
+
// ── sec-fetch-mismatch CDN marker emission ──────────────────────────────────
|
|
295
|
+
//
|
|
296
|
+
// When the CDN sees sec-fetch-dest: document + sec-fetch-mode: navigate on a
|
|
297
|
+
// vai.json request, it appends sec-fetch-mismatch to X-PW-UA. This marker is
|
|
298
|
+
// used by the cloud-api to reclassify the request as OTHER without inspecting
|
|
299
|
+
// raw Sec-Fetch headers in the classification path.
|
|
300
|
+
|
|
301
|
+
describe('CDN sec-fetch-mismatch marker emission', () => {
|
|
302
|
+
let handler;
|
|
303
|
+
|
|
304
|
+
beforeAll(async () => {
|
|
305
|
+
handler = await init('cloudflare');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
async function proxyVAI(url, headerMap, cf) {
|
|
309
|
+
const request = makeRequest(url, headerMap, cf);
|
|
310
|
+
capturedFetchArgs = null;
|
|
311
|
+
await handler(request, ENV, CTX);
|
|
312
|
+
return capturedFetchArgs;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
test('document/navigate on vai.json → appends sec-fetch-mismatch', async () => {
|
|
316
|
+
const mismatchHeaders = {
|
|
317
|
+
...CHROME_MAC_HEADERS,
|
|
318
|
+
'sec-fetch-dest': 'document',
|
|
319
|
+
'sec-fetch-mode': 'navigate',
|
|
320
|
+
};
|
|
321
|
+
const args = await proxyVAI('https://pub.example.com/pw/vai.json', mismatchHeaders, CF_PROPS);
|
|
322
|
+
expect(args.headers['X-PW-UA']).toContain('sec-fetch-mismatch');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('empty/cors on vai.json (normal XHR) → no sec-fetch-mismatch', async () => {
|
|
326
|
+
const args = await proxyVAI('https://pub.example.com/pw/vai.json', CHROME_MAC_HEADERS, CF_PROPS);
|
|
327
|
+
expect(args.headers['X-PW-UA']).not.toContain('sec-fetch-mismatch');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test('document/navigate on non-vai path → no sec-fetch-mismatch', async () => {
|
|
331
|
+
const mismatchHeaders = {
|
|
332
|
+
...CHROME_MAC_HEADERS,
|
|
333
|
+
'sec-fetch-dest': 'document',
|
|
334
|
+
'sec-fetch-mode': 'navigate',
|
|
335
|
+
};
|
|
336
|
+
const args = await proxyVAI('https://pub.example.com/pw/access/check', mismatchHeaders, CF_PROPS);
|
|
337
|
+
// sec-fetch-mismatch only applies to vai.json requests
|
|
338
|
+
if (args && args.headers) {
|
|
339
|
+
expect(args.headers['X-PW-UA'] || '').not.toContain('sec-fetch-mismatch');
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
294
344
|
// ── logAccess — non-VAI path raw header forwarding ───────────────────────────
|
|
295
345
|
//
|
|
296
346
|
// Verifies that logAccess() does NOT transform headers like proxyVAIRequest().
|