263 lines
10 KiB
Markdown
263 lines
10 KiB
Markdown
# Advanced Form Automation Techniques
|
|
|
|
> Techniques discovered during TJC (Dynamic Forms / NGWebSolutions) account registration automation. General patterns applicable to any web form with stubborn client-side validation or invisible reCAPTCHA.
|
|
|
|
## 1. Password Field Bypass via `setNativeValue` + `dispatchEvent`
|
|
|
|
### The Problem
|
|
|
|
Some forms use custom JavaScript validation that checks for specific input events before accepting a password field. Standard Playwright methods fail:
|
|
|
|
| Method | Result |
|
|
|--------|--------|
|
|
| `page.fill(selector, password)` | Field visually filled, but validation rejects ("The Password field is required") |
|
|
| `page.type(selector, password)` | Same result — validation rejects |
|
|
| `page.pressSequentially(selector, password)` | Same result |
|
|
| `page.locator(selector).fill(password)` | Same result |
|
|
|
|
The root cause: the form's JavaScript validation checks for `input` events dispatched by the native `EventTarget.prototype.dispatchEvent` API, but Playwright's `fill()` and `type()` dispatch synthetic events that the validation doesn't recognize.
|
|
|
|
### The Fix
|
|
|
|
Use `page.evaluate()` to set the value natively and dispatch the event manually:
|
|
|
|
```javascript
|
|
// In page.evaluate():
|
|
const input = document.querySelector('input#Password');
|
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
|
|
window.HTMLInputElement.prototype, 'value'
|
|
).set;
|
|
nativeInputValueSetter.call(input, 'YourPasswordHere123@');
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
```
|
|
|
|
### Why it works
|
|
|
|
- `Object.getOwnPropertyDescriptor(..., 'value').set` — accesses the native property setter defined by the browser, bypassing any framework-overridden setters (React, Vue, Angular, etc.)
|
|
- `.call(input, value)` — invokes the setter directly on the element, setting the internal value without triggering the framework's synthetic event system
|
|
- `dispatchEvent(new Event('input', { bubbles: true }))` — fires a browser-native input event that the form's validation listener recognizes
|
|
|
|
### Pattern (Python + Playwright)
|
|
|
|
```python
|
|
async def set_value_native(page, selector, value):
|
|
"""Set form field value bypassing JS framework validation"""
|
|
await page.evaluate(f"""
|
|
(() => {{
|
|
const input = document.querySelector('{selector}');
|
|
const setter = Object.getOwnPropertyDescriptor(
|
|
window.HTMLInputElement.prototype, 'value'
|
|
).set;
|
|
setter.call(input, '{value}');
|
|
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
|
|
}})()
|
|
""")
|
|
```
|
|
|
|
**⚠️ Pitfall: escaping special characters.** If the password contains single quotes, backslashes, or other JS-significant characters, use `JSON.stringify(value)` or pass the value as a function argument:
|
|
|
|
```python
|
|
await page.evaluate("""
|
|
([selector, value]) => {
|
|
const input = document.querySelector(selector);
|
|
const setter = Object.getOwnPropertyDescriptor(
|
|
window.HTMLInputElement.prototype, 'value'
|
|
).set;
|
|
setter.call(input, value);
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
}
|
|
""", [selector, password])
|
|
```
|
|
|
|
### When to use this
|
|
|
|
- Playwright `fill()` / `type()` / `pressSequentially()` all fail
|
|
- The form shows "This field is required" even though the field visually has content
|
|
- The form uses custom JavaScript validation (common in ASP.NET MVC, Angular, React forms)
|
|
- Specifically for password fields — text fields rarely have this issue
|
|
|
|
### Verification
|
|
|
|
After calling `setValueNative`, verify the field actually has the value:
|
|
|
|
```javascript
|
|
document.querySelector('input#Password').value.length // Should be > 0
|
|
```
|
|
|
|
## 2. Invisible reCAPTCHA with `g-recaptcha` Class
|
|
|
|
### Identifying invisible reCAPTCHA
|
|
|
|
The form button has these attributes:
|
|
|
|
```html
|
|
<button class="btn btn-default g-recaptcha"
|
|
data-sitekey="6Lc1DN8ZAAAAAJ24AatUEzMn95wjUkjmHt65BfQi"
|
|
data-callback="onSubmit"
|
|
data-action="CreateAccount">
|
|
Create Account
|
|
</button>
|
|
```
|
|
|
|
Key signals:
|
|
- **`class="g-recaptcha"`** — Google's CSS class for automatic binding
|
|
- **`data-callback="onSubmit"`** — function called when reCAPTCHA challenge succeeds
|
|
- **`data-sitekey="..."`** — the reCAPTCHA site key
|
|
- **No `type="submit"`** — the button is NOT a submit button; reCAPTCHA handles submission
|
|
|
|
### How it works
|
|
|
|
1. Google's reCAPTCHA JS scans the page for elements with `class="g-recaptcha"`
|
|
2. When the button is clicked, reCAPTCHA intercepts the click
|
|
3. Runs the invisible challenge (no user interaction required)
|
|
4. On success, calls `onSubmit(token)` — which sets the token in a hidden field and submits the form
|
|
|
|
### The `onSubmit` callback pattern
|
|
|
|
Typically looks like:
|
|
|
|
```javascript
|
|
function onSubmit(token) {
|
|
$("#RecaptchaToken").val(token);
|
|
document.getElementById("createAccountForm").submit();
|
|
}
|
|
```
|
|
|
|
### Hidden field for token
|
|
|
|
The form has a hidden input that stores the reCAPTCHA token:
|
|
|
|
[REDACTED]
|
|
<input id="RecaptchaToken" name="RecaptchaToken" type="hidden" value="" />
|
|
```
|
|
|
|
This is DIFFERENT from the standard `g-recaptcha-response` hidden field. Some custom implementations use their own field name.
|
|
|
|
### Approach 1: Solve via 2captcha + manual injection
|
|
|
|
```python
|
|
import requests, time
|
|
|
|
CAPTCHA_KEY = "your_2captcha_key"
|
|
SITEKEY = "6Lc1DN8ZAAAAAJ24AatUEzMn95wjUkjmHt65BfQi"
|
|
PAGEURL = "https://target.com/register"
|
|
|
|
# 1. Submit to 2captcha
|
|
resp = requests.get("https://2captcha.com/in.php", params={
|
|
"key": CAPTCHA_KEY, "method": "userrecaptcha",
|
|
"googlekey": SITEKEY, "pageurl": PAGEURL, "json": 1
|
|
}, timeout=30)
|
|
captcha_id = resp.json()["request"]
|
|
|
|
# 2. Poll for result
|
|
for i in range(20):
|
|
time.sleep(5)
|
|
resp = requests.get("https://2captcha.com/res.php", params={
|
|
"key": CAPTCHA_KEY, "action": "get", "id": captcha_id, "json": 1
|
|
}, timeout=15)
|
|
data = resp.json()
|
|
if data.get("status") == 1:
|
|
token = data["request"]
|
|
break
|
|
|
|
# 3. Inject into browser (via page.evaluate or browser_console)
|
|
await page.evaluate("""
|
|
(token) => {
|
|
document.getElementById('RecaptchaToken').value = token;
|
|
}
|
|
""", token)
|
|
|
|
# 4. Trigger onSubmit
|
|
await page.evaluate("""
|
|
onSubmit(document.getElementById('RecaptchaToken').value);
|
|
""")
|
|
```
|
|
|
|
### Approach 2: Browser-native `grecaptcha.execute()`
|
|
|
|
If the reCAPTCHA script is loaded and the widget is rendered:
|
|
|
|
```javascript
|
|
// In browser_console or page.evaluate:
|
|
grecaptcha.execute();
|
|
```
|
|
|
|
This returns a Promise that resolves to a fresh token. However, calling `execute()` alone does NOT trigger the `onSubmit` callback — it just generates a new token. You still need to either:
|
|
- click the `g-recaptcha` button (which internally calls execute + callback)
|
|
- manually call `onSubmit(token)` after getting the token
|
|
|
|
### Approach 3: Click the button (preferred)
|
|
|
|
If the password field is filled correctly (via `setNativeValue`), simply clicking the `g-recaptcha` button triggers the full flow:
|
|
|
|
```javascript
|
|
document.querySelector('.g-recaptcha').click();
|
|
```
|
|
|
|
The reCAPTCHA library intercepts the click, runs the challenge, and calls `onSubmit` on success.
|
|
|
|
### Known issues
|
|
|
|
- **Migration warning**: Some reCAPTCHA site keys show "Google reCAPTCHA migration" warnings in the console. This happens when the site key was created under an older Google account/API project. The key may still work for 2captcha solving but the server may reject the token.
|
|
- **Token expiry**: reCAPTCHA tokens expire after ~2 minutes. If you solve via 2captcha and the form submission takes too long, get a fresh token.
|
|
- **Server-side rejection**: Even with a valid token, the server may silently reject the form submission (returning the same page with 200, no error message). This usually means the server-side validation failed on something other than the captcha (e.g., duplicate username, email format, anti-forgery token mismatch).
|
|
|
|
## 3. ASP.NET Anti-Forgery Token Handling
|
|
|
|
ASP.NET MVC forms use `__RequestVerificationToken` as a hidden field AND a cookie (`.__AspNetCore.Antiforgery.*`). Both must match.
|
|
|
|
### In the browser
|
|
|
|
The browser automatically handles this — the cookie is set on page load, and the hidden field value is embedded in the form. `form.submit()` sends both correctly.
|
|
|
|
### Via curl/requests
|
|
|
|
1. GET the form page to receive the cookie
|
|
2. Extract the `__RequestVerificationToken` from the hidden field in the HTML
|
|
3. POST with both the cookie and the token value
|
|
|
|
```bash
|
|
# Step 1: GET form (stores cookie)
|
|
curl -c cookies.txt -b cookies.txt -L 'https://target.com/Account/CreateAccount'
|
|
|
|
# Step 2: Extract token from HTML
|
|
TOKEN=$(grep -oP '__RequestVerificationToken" value="\K[^"]+' cookies.txt || \
|
|
grep -oP '__RequestVerificationToken" value="\K[^"]+' response.html)
|
|
|
|
# Step 3: POST with form data
|
|
curl -c cookies.txt -b cookies.txt \
|
|
-X POST 'https://target.com/Account/CreateAccount' \
|
|
-d "__RequestVerificationToken=$TOKEN" \
|
|
-d "Username=testuser" \
|
|
-d "Password=[REDACTED]"
|
|
```
|
|
|
|
**Without the correct token**: server returns HTTP 400 Bad Request.
|
|
|
|
## 4. Chromium SOCKS5 Proxy (unauthenticated)
|
|
|
|
Playwright/Chromium does NOT support SOCKS5 with authentication. Workaround: set up an SSH tunnel without auth:
|
|
|
|
```bash
|
|
# Create tunnel (no auth needed)
|
|
ssh -D 1081 -N root@your-server.com
|
|
|
|
# In Playwright
|
|
browser = await playwright.chromium.launch(
|
|
args=["--proxy-server=socks5://localhost:1081"]
|
|
)
|
|
```
|
|
|
|
The tunnel must be running before launching the browser. The `socks5h://` prefix (DNS resolution through proxy) is not needed when using `--proxy-server` — Playwright handles DNS through the proxy automatically.
|
|
|
|
## 5. Diagnostic Flow for Form Submission Failures
|
|
|
|
When a form submits but returns the same page (no error, no redirect):
|
|
|
|
1. **Check password validation** — inspect `document.getElementById('Password').value.length` → if 0, the password was cleared by the page reload. Use `setNativeValue` technique.
|
|
2. **Check reCAPTCHA token** — inspect `document.getElementById('RecaptchaToken').value` → if empty or stale, re-solve.
|
|
3. **Check anti-forgery token** — inspect `document.querySelector('input[name=__RequestVerificationToken]').value` → if empty, the page state reset.
|
|
4. **Check for validation errors** — `document.querySelector('.validation-summary-errors, .text-danger')?` → if present, read the error text.
|
|
5. **Check browser console** — `console.clear()` then `console.log('ready')` before action, check for JS errors after.
|
|
6. **Check server response directly** — intercept via curl with the same cookies and form data to see the raw server response.
|
|
7. **Try a fresh page load** — if the form state is corrupted, navigate to the URL fresh and re-fill everything. |