Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions injected/src/detectors/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
# Web Interference Detection

This directory contains web interference detection functionality for content-scope-scripts. Detectors identify CAPTCHAs, fraud warnings, and other interference patterns to support breakage reporting and PIR automation.

## Architecture

The system provides simple detection utilities that can be called on-demand:

- **Detection utilities** - Pure functions (`runBotDetection`, `runFraudDetection`) that scan DOM when called
- **Direct imports** - Features (breakage reporting, PIR) import detection functions directly
- **`WebInterferenceDetection`** - Optional ContentFeature wrapper for messaging (PIR use, not currently bundled)


## Directory Layout

```
detectors/
├── detections/
│ ├── bot-detection.js # CAPTCHA/bot detection utility
│ └── fraud-detection.js # fraud/phishing warning utility
├── utils/
│ └── detection-utils.js # DOM helpers (selectors, text matching, visibility)
└── default-config.js # fallback detector settings
```

## How It Works

### 1. On-Demand Detection

Detectors are simple functions that scan the DOM when called:

1. Feature imports detector function (e.g., `runBotDetection`)
2. Feature calls detector with config when needed (e.g., user submits breakage report)
3. Detector scans DOM and returns results immediately (~few ms)
4. No caching - each call is fresh

### 2. Configuration

Detectors are configured via `privacy-configuration/features/web-interference-detection.json`:

```json
{
"state": "enabled",
"settings": {
"autoRunDelayMs": 100,
"interferenceTypes": {
"botDetection": {
"hcaptcha": {
"state": "enabled",
"vendor": "hcaptcha",
"selectors": [".h-captcha"],
"windowProperties": ["hcaptcha"]
}
},
"fraudDetection": {
"phishingWarning": {
"state": "enabled",
"type": "phishing",
"selectors": [".warning-banner"]
}
}
}
}
}
```

**Domain-specific configuration** using `conditionalChanges`:

```json
{
"settings": {
"conditionalChanges": [
{
"condition": {
"urlPattern": "https://*.example.com/*"
},
"patchSettings": [
{
"op": "add",
"path": "/interferenceTypes/customDetector",
"value": { "state": "enabled", "selectors": [".custom"] }
}
]
}
]
}
}
```

The framework automatically applies conditional changes based on the current URL before passing settings to the feature.

### 3. Using Detection Results

**Breakage reporting** (internal feature):

```javascript
import { runBotDetection, runFraudDetection } from '../detectors/detections/bot-detection.js';

// Get detector config from privacy-configuration
const detectorSettings = this.getFeatureSetting('webInterferenceDetection', 'interferenceTypes');

if (detectorSettings) {
const result = {
botDetection: runBotDetection(detectorSettings.botDetection),
fraudDetection: runFraudDetection(detectorSettings.fraudDetection),
};
}
```

**PIR/native** (via messaging, when `WebInterferenceDetection` is bundled):

```javascript
// Via messaging
this.messaging.request('detectInterference', {
types: ['botDetection', 'fraudDetection']
});
```

## Adding New Detectors

1. **Create detection utility** in `detections/`:

```javascript
// detections/my-detector.js
export function runMyDetection(config = {}) {
// Run detection logic
const detected = checkSelectors(config.selectors);

return {
detected,
type: 'myDetector',
results: [...],
};
}
```

2. **Use in breakage reporting or other feature**:

```javascript
// features/breakage-reporting.js
import { runMyDetection } from '../detectors/detections/my-detector.js';

const detectorSettings = this.getFeatureSetting('webInterferenceDetection', 'interferenceTypes');
if (detectorSettings?.myDetector) {
result.myDetectorData = runMyDetection(detectorSettings.myDetector);
}
```

3. **Add config** to `web-interference-detection.json`:

```json
{
"settings": {
"interferenceTypes": {
"myDetector": {
"state": "enabled",
"selectors": [".my-selector"]
}
}
}
}
```

## Performance

- Detectors are simple DOM queries - typically < 5ms
- No caching overhead or stale results
- Only run when explicitly needed (e.g., breakage report submitted)
- Future: If frequent polling is needed, add a shared caching wrapper
53 changes: 53 additions & 0 deletions injected/src/detectors/default-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export const DEFAULT_DETECTOR_SETTINGS = Object.freeze({
botDetection: {
cloudflareTurnstile: {
state: 'enabled',
vendor: 'cloudflare',
selectors: ['.cf-turnstile', 'script[src*="challenges.cloudflare.com"]'],
windowProperties: ['turnstile'],
statusSelectors: [
{
status: 'solved',
selectors: ['[data-state="success"]'],
},
{
status: 'failed',
selectors: ['[data-state="error"]'],
},
],
},
cloudflareChallengePage: {
state: 'enabled',
vendor: 'cloudflare',
selectors: ['#challenge-form', '.cf-browser-verification', '#cf-wrapper', 'script[src*="challenges.cloudflare.com"]'],
windowProperties: ['_cf_chl_opt', '__CF$cv$params', 'cfjsd'],
},
hcaptcha: {
state: 'enabled',
vendor: 'hcaptcha',
selectors: [
'.h-captcha',
'[data-hcaptcha-widget-id]',
'script[src*="hcaptcha.com"]',
'script[src*="assets.hcaptcha.com"]',
],
windowProperties: ['hcaptcha'],
},
},
fraudDetection: {
phishingWarning: {
state: 'enabled',
type: 'phishing',
selectors: ['.warning-banner', '#security-alert'],
textPatterns: ['suspicious.*activity', 'unusual.*login', 'verify.*account'],
textSources: ['innerText'],
},
accountSuspension: {
state: 'enabled',
type: 'suspension',
selectors: ['.account-suspended', '#suspension-notice'],
textPatterns: ['account.*suspended', 'access.*restricted'],
textSources: ['innerText'],
},
},
});
45 changes: 45 additions & 0 deletions injected/src/detectors/detections/bot-detection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { checkSelectors, checkWindowProperties, matchesSelectors, matchesTextPatterns } from '../utils/detection-utils.js';

/**
* Run bot detection.
* @param {Record<string, any>} config
*/
export function runBotDetection(config = {}) {
const results = Object.entries(config)
.filter(([_, challengeConfig]) => challengeConfig?.state === 'enabled')
.map(([challengeId, challengeConfig]) => {
const detected = checkSelectors(challengeConfig.selectors) || checkWindowProperties(challengeConfig.windowProperties || []);
if (!detected) {
return null;
}

const challengeStatus = findStatus(challengeConfig.statusSelectors);
return {
detected: true,
vendor: challengeConfig.vendor,
challengeType: challengeId,
challengeStatus,
};
})
.filter(Boolean);

return {
detected: results.length > 0,
type: 'botDetection',
results,
};
}

function findStatus(statusSelectors) {
if (!Array.isArray(statusSelectors)) {
return null;
}

const match = statusSelectors.find((statusConfig) => {
const { selectors, textPatterns, textSources } = statusConfig;
return matchesSelectors(selectors) || matchesTextPatterns(document.body, textPatterns, textSources);
});

return match?.status ?? null;
}

31 changes: 31 additions & 0 deletions injected/src/detectors/detections/fraud-detection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { checkSelectorsWithVisibility, checkTextPatterns } from '../utils/detection-utils.js';

/**
* Run fraud detection.
* @param {Record<string, any>} config
*/
export function runFraudDetection(config = {}) {
const results = Object.entries(config)
.filter(([_, alertConfig]) => alertConfig?.state === 'enabled')
.map(([alertId, alertConfig]) => {
const detected =
checkSelectorsWithVisibility(alertConfig.selectors) ||
checkTextPatterns(alertConfig.textPatterns, alertConfig.textSources);
if (!detected) {
return null;
}

return {
detected: true,
alertId,
category: alertConfig.type,
};
})
.filter(Boolean);

return {
detected: results.length > 0,
type: 'fraudDetection',
results,
};
}
Loading
Loading