Skip to content

Commit 8b1558f

Browse files
committed
extend stealth
1 parent 12345ed commit 8b1558f

File tree

2 files changed

+181
-58
lines changed

2 files changed

+181
-58
lines changed

README.md

Lines changed: 127 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,30 @@ dotnet add package ManagedCode.Playwright.Stealth
1414
using Microsoft.Playwright;
1515
using ManagedCode.Playwright.Stealth;
1616

17+
using var playwright = await Playwright.CreateAsync();
18+
19+
// One-call launch with stealth args + context pre-configured:
20+
var (browser, context) = await playwright.Chromium.LaunchStealthAsync();
21+
22+
var page = await context.NewPageAsync();
23+
await page.GotoAsync("https://www.browserscan.net/bot-detection");
24+
```
25+
26+
## Manual Setup
27+
28+
If you need more control over launch options:
29+
30+
```csharp
1731
using var playwright = await Playwright.CreateAsync();
1832
await using var browser = await playwright.Chromium.LaunchAsync(new BrowserTypeLaunchOptions
1933
{
20-
Headless = true
34+
Headless = true,
35+
Args = PlaywrightStealthExtensions.StealthArgs // recommended Chrome flags
2136
});
2237

2338
var context = await browser.NewContextAsync();
2439

25-
// Apply ManagedCode.Playwright.Stealth before creating pages.
40+
// Apply stealth before creating pages.
2641
await context.ApplyStealthAsync();
2742

2843
var page = await context.NewPageAsync();
@@ -35,25 +50,21 @@ await page.GotoAsync("https://www.browserscan.net/bot-detection");
3550
var config = new StealthConfig
3651
{
3752
NavigatorHardwareConcurrency = 8,
38-
NavigatorLanguages = true,
53+
NavigatorDeviceMemory = 16,
54+
NavigatorMaxTouchPoints = 1,
3955
NavigatorUserAgentValue = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
40-
WebglVendor = true
56+
Vendor = "Intel Inc.",
57+
Renderer = "Intel Iris OpenGL Engine"
4158
};
4259

43-
// Apply ManagedCode.Playwright.Stealth with a custom config.
4460
await context.ApplyStealthAsync(config);
4561
```
4662

47-
## Usage Variants (C#/.NET)
48-
4963
Apply stealth on a page (if you already have a page instance):
5064

5165
```csharp
5266
var page = await context.NewPageAsync();
53-
54-
// Apply ManagedCode.Playwright.Stealth to an existing page.
5567
await page.ApplyStealthAsync();
56-
5768
await page.GotoAsync("https://www.browserscan.net/bot-detection");
5869
```
5970

@@ -64,82 +75,140 @@ var config = new StealthConfig
6475
{
6576
WebDriver = false,
6677
WebglVendor = false,
67-
NavigatorLanguages = false,
68-
NavigatorPlugins = false,
69-
ChromeRuntime = false
78+
CanvasFingerprint = false,
79+
AudioContext = false,
80+
PerformanceJitter = false
7081
};
7182

72-
// Apply ManagedCode.Playwright.Stealth with selective patches disabled.
7383
await context.ApplyStealthAsync(config);
7484
```
7585

76-
Customize platform, vendor, and WebGL identity:
86+
## Patched Signals
87+
88+
The default configuration patches **31 detection vectors** across these categories:
89+
90+
### Navigator Properties
91+
| Patch | Description | Config |
92+
|-------|-------------|--------|
93+
| `navigator.webdriver` | Returns `false` instead of `true` | `WebDriver` |
94+
| `navigator.plugins` / `mimeTypes` | Fake plugin array (Chrome PDF Plugin, etc.) | `NavigatorPlugins` |
95+
| `navigator.languages` | Configurable language array | `NavigatorLanguages` |
96+
| `navigator.userAgent` | Strips headless markers from UA string | `NavigatorUserAgent` |
97+
| `navigator.vendor` | Returns "Google Inc." | `NavigatorVendor` |
98+
| `navigator.platform` | Configurable platform string | `NavigatorPlatform` |
99+
| `navigator.hardwareConcurrency` | Configurable CPU core count (default: 4) | `NavigatorHardwareConcurrency` |
100+
| `navigator.deviceMemory` | Configurable device memory in GB (default: 8) | `NavigatorDeviceMemory` |
101+
| `navigator.connection` | Full NetworkInformation API mock (4g, 10 Mbps) | `NavigatorConnection` |
102+
| `navigator.permissions` | Normalizes Notification permission state | `NavigatorPermissions` |
103+
| `navigator.maxTouchPoints` | Configurable touch point count (default: 1) | `NavigatorMaxTouchPoints` |
104+
| `navigator.pdfViewerEnabled` | Returns `true` | `NavigatorPdfViewer` |
105+
106+
### Chrome APIs
107+
| Patch | Description | Config |
108+
|-------|-------------|--------|
109+
| `window.chrome` / `chrome.runtime` | Full Chrome extension API mock | `ChromeRuntime` |
110+
| `chrome.app` | Chrome App API mock | `ChromeApp` |
111+
| `chrome.csi` | Chrome CSI timing mock | `ChromeCsi` |
112+
| `chrome.loadTimes` | Chrome load times mock | `ChromeLoadTimes` |
113+
114+
### Graphics & Rendering
115+
| Patch | Description | Config |
116+
|-------|-------------|--------|
117+
| WebGL vendor/renderer | Spoofs UNMASKED and standard WebGL params; hides ANGLE/SwiftShader | `WebglVendor` |
118+
| Canvas fingerprint | Adds session-stable noise to canvas rendering | `CanvasFingerprint` |
119+
| Broken image dimensions | Fixes 16x16 headless artifact to 0x0 | `BrokenImage` |
120+
121+
### Audio & Media
122+
| Patch | Description | Config |
123+
|-------|-------------|--------|
124+
| AudioContext fingerprint | Adds noise to audio frequency/channel data | `AudioContext` |
125+
| Media codecs | Correct codec support responses | `MediaCodecs` |
126+
| Speech synthesis | Mock voice list for `getVoices()` | `SpeechSynthesis` |
127+
128+
### Window & Screen
129+
| Patch | Description | Config |
130+
|-------|-------------|--------|
131+
| `window.outerWidth/Height` | Realistic outer dimensions | `OuterDimensions` |
132+
| `screen.*` dimensions | Consistent screen width/height/colorDepth | `ScreenDimensions` |
133+
134+
### Anti-Detection & Timing
135+
| Patch | Description | Config |
136+
|-------|-------------|--------|
137+
| CDP detection | Masks Chrome DevTools Protocol traces | `CdpDetection` |
138+
| Automation properties | Removes `cdc_*`, `$cdc_*`, `domAutomationController` | `AutomationProperties` |
139+
| Performance jitter | Adds realistic timing noise to `performance.now()` and `requestAnimationFrame` | `PerformanceJitter` |
140+
141+
### DOM & Internals
142+
| Patch | Description | Config |
143+
|-------|-------------|--------|
144+
| iframe `contentWindow` | Fixes iframe proxy behavior | `IframeContentWindow` |
145+
| Hairline detection | Fixes Modernizr `offsetHeight` check | `Hairline` |
146+
147+
## Public API
148+
149+
### Extension Methods
77150

78151
```csharp
79-
var config = new StealthConfig
80-
{
81-
NavigatorPlatformValue = "Win32",
82-
NavigatorVendorValue = "Google Inc.",
83-
NavigatorUserAgentValue = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
84-
Vendor = "Intel Inc.",
85-
Renderer = "Intel Iris OpenGL Engine",
86-
Languages = new[] { "en-US", "en" },
87-
NavigatorHardwareConcurrency = 8
88-
};
152+
// Apply to browser context (recommended)
153+
await context.ApplyStealthAsync();
154+
await context.ApplyStealthAsync(customConfig);
89155

90-
// Apply ManagedCode.Playwright.Stealth with explicit platform and WebGL identity.
91-
await context.ApplyStealthAsync(config);
156+
// Apply to individual page
157+
await page.ApplyStealthAsync();
158+
await page.ApplyStealthAsync(customConfig);
159+
160+
// One-call launch with stealth pre-configured
161+
var (browser, context) = await playwright.Chromium.LaunchStealthAsync();
162+
var (browser, context) = await playwright.Chromium.LaunchStealthAsync(config, launchOptions, contextOptions);
92163
```
93164

94-
Run on insecure origins (affects `chrome.runtime` behavior):
165+
### Stealth Chrome Arguments
95166

96167
```csharp
97-
var config = new StealthConfig
98-
{
99-
RunOnInsecureOrigins = true
100-
};
101-
102-
// Apply ManagedCode.Playwright.Stealth on insecure origins.
103-
await context.ApplyStealthAsync(config);
168+
// Access recommended Chrome args for manual launch setup
169+
string[] args = PlaywrightStealthExtensions.StealthArgs;
104170
```
105171

106-
## Patched Signals
172+
## Configuration Reference
107173

108-
The default configuration applies patches for:
109-
110-
- `navigator.webdriver`
111-
- `navigator.plugins` and `navigator.mimeTypes`
112-
- `navigator.languages`
113-
- `navigator.userAgent`
114-
- `navigator.vendor`
115-
- `navigator.platform`
116-
- `navigator.hardwareConcurrency`
117-
- `window.chrome` / `chrome.runtime` / related Chrome APIs
118-
- WebGL vendor/renderer
119-
- `window.outerWidth` / `window.outerHeight`
120-
- media codecs and hairline fixes
121-
- iframe `contentWindow` quirks
174+
### Toggle Options (bool)
122175

123-
## Configuration Reference
176+
`WebDriver`, `WebglVendor`, `ChromeApp`, `ChromeCsi`, `ChromeLoadTimes`, `ChromeRuntime`,
177+
`IframeContentWindow`, `MediaCodecs`, `Hairline`, `OuterDimensions`,
178+
`NavigatorLanguages`, `NavigatorPermissions`, `NavigatorPlatform`, `NavigatorPlugins`,
179+
`NavigatorUserAgent`, `NavigatorVendor`, `NavigatorConnection`, `NavigatorPdfViewer`,
180+
`BrokenImage`, `SpeechSynthesis`, `ScreenDimensions`,
181+
`CdpDetection`, `AutomationProperties`, `CanvasFingerprint`, `PerformanceJitter`, `AudioContext`
182+
183+
### Numeric Options (int)
184+
185+
- `NavigatorHardwareConcurrency` (default: 4, set 0 to disable)
186+
- `NavigatorDeviceMemory` (default: 8, set 0 to disable)
187+
- `NavigatorMaxTouchPoints` (default: 1, set -1 to disable)
124188

125-
Common options in `StealthConfig`:
189+
### String Options
126190

127-
- `WebDriver`, `WebglVendor`, `ChromeApp`, `ChromeCsi`, `ChromeLoadTimes`, `ChromeRuntime`
128-
- `IframeContentWindow`, `MediaCodecs`, `Hairline`, `OuterDimensions`
129-
- `NavigatorLanguages`, `NavigatorPermissions`, `NavigatorPlatform`, `NavigatorPlugins`, `NavigatorUserAgent`, `NavigatorVendor`
130-
- `NavigatorHardwareConcurrency`, `NavigatorUserAgentValue`, `NavigatorPlatformValue`, `NavigatorVendorValue`
131-
- `Vendor`, `Renderer`, `Languages`, `RunOnInsecureOrigins`
191+
- `NavigatorUserAgentValue` - Custom user agent string
192+
- `NavigatorPlatformValue` - Custom platform (e.g., "Win32")
193+
- `NavigatorVendorValue` - Vendor name (default: "Google Inc.")
194+
- `Vendor` - WebGL vendor (default: "Intel Inc.")
195+
- `Renderer` - WebGL renderer (default: "Intel Iris OpenGL Engine")
196+
- `Languages` - Language array (default: `["en-US", "en"]`)
197+
- `RunOnInsecureOrigins` - Allow stealth on http:// origins
132198

133199
## Testing
134200

135-
Integration tests target these bot-detection sites:
201+
Integration tests target **9 bot-detection sites**:
136202

137203
- https://www.browserscan.net/bot-detection
138204
- https://bot.sannysoft.com/
139205
- https://www.intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html
140206
- https://fingerprint.com/demo
141207
- https://arh.antoinevastel.com/bots/areyouheadless/
142208
- https://pixelscan.net/bot-check
209+
- https://bot.incolumitas.com/
210+
- https://abrahamjuliot.github.io/creepjs/
211+
- https://deviceandbrowserinfo.com/info_device
143212

144213
These sites can change at any time. If a site changes, update the corresponding test assertions.
145214

src/Playwright.Stealth/PlaywrightStealthExtensions.cs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ namespace ManagedCode.Playwright.Stealth;
44

55
public static class PlaywrightStealthExtensions
66
{
7+
/// <summary>
8+
/// Recommended Chrome arguments to reduce bot-detection surface.
9+
/// Pass these to <see cref="BrowserTypeLaunchOptions.Args"/>.
10+
/// </summary>
11+
public static readonly string[] StealthArgs =
12+
[
13+
"--disable-blink-features=AutomationControlled",
14+
"--disable-default-apps",
15+
"--no-first-run",
16+
"--no-default-browser-check",
17+
"--disable-component-update",
18+
"--disable-client-side-phishing-detection",
19+
"--disable-hang-monitor",
20+
"--disable-breakpad",
21+
"--metrics-recording-only"
22+
];
23+
24+
/// <summary>
25+
/// Apply stealth evasion scripts to an existing page.
26+
/// Call <b>before</b> navigating to the target URL.
27+
/// </summary>
728
public static async Task ApplyStealthAsync(this IPage page, StealthConfig? config = null)
829
{
930
var stealthConfig = config ?? new StealthConfig();
@@ -13,6 +34,10 @@ public static async Task ApplyStealthAsync(this IPage page, StealthConfig? confi
1334
}
1435
}
1536

37+
/// <summary>
38+
/// Apply stealth evasion scripts to a browser context.
39+
/// Call <b>before</b> creating pages. All pages created from this context will inherit the stealth scripts.
40+
/// </summary>
1641
public static async Task ApplyStealthAsync(this IBrowserContext context, StealthConfig? config = null)
1742
{
1843
var stealthConfig = config ?? new StealthConfig();
@@ -21,4 +46,33 @@ public static async Task ApplyStealthAsync(this IBrowserContext context, Stealth
2146
await context.AddInitScriptAsync(script).ConfigureAwait(false);
2247
}
2348
}
49+
50+
/// <summary>
51+
/// Launch Chromium with stealth arguments and create a context with stealth scripts applied.
52+
/// This is the recommended one-call setup for most use cases.
53+
/// </summary>
54+
public static async Task<(IBrowser Browser, IBrowserContext Context)> LaunchStealthAsync(
55+
this IBrowserType browserType,
56+
StealthConfig? config = null,
57+
BrowserTypeLaunchOptions? launchOptions = null,
58+
BrowserNewContextOptions? contextOptions = null)
59+
{
60+
var opts = launchOptions ?? new BrowserTypeLaunchOptions();
61+
opts.Args = opts.Args is null
62+
? StealthArgs
63+
: [..opts.Args, ..StealthArgs];
64+
65+
var browser = await browserType.LaunchAsync(opts).ConfigureAwait(false);
66+
67+
var ctxOpts = contextOptions ?? new BrowserNewContextOptions
68+
{
69+
ViewportSize = new ViewportSize { Width = 1920, Height = 1080 },
70+
DeviceScaleFactor = 2
71+
};
72+
73+
var context = await browser.NewContextAsync(ctxOpts).ConfigureAwait(false);
74+
await context.ApplyStealthAsync(config).ConfigureAwait(false);
75+
76+
return (browser, context);
77+
}
2478
}

0 commit comments

Comments
 (0)