10 KiB
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:
// 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 systemdispatchEvent(new Event('input', { bubbles: true }))— fires a browser-native input event that the form's validation listener recognizes
Pattern (Python + Playwright)
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:
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:
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:
<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 bindingdata-callback="onSubmit"— function called when reCAPTCHA challenge succeedsdata-sitekey="..."— the reCAPTCHA site key- No
type="submit"— the button is NOT a submit button; reCAPTCHA handles submission
How it works
- Google's reCAPTCHA JS scans the page for elements with
class="g-recaptcha" - When the button is clicked, reCAPTCHA intercepts the click
- Runs the invisible challenge (no user interaction required)
- On success, calls
onSubmit(token)— which sets the token in a hidden field and submits the form
The onSubmit callback pattern
Typically looks like:
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]
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:
// 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-recaptchabutton (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:
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
- GET the form page to receive the cookie
- Extract the
__RequestVerificationTokenfrom the hidden field in the HTML - POST with both the cookie and the token value
# 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:
# 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):
- Check password validation — inspect
document.getElementById('Password').value.length→ if 0, the password was cleared by the page reload. UsesetNativeValuetechnique. - Check reCAPTCHA token — inspect
document.getElementById('RecaptchaToken').value→ if empty or stale, re-solve. - Check anti-forgery token — inspect
document.querySelector('input[name=__RequestVerificationToken]').value→ if empty, the page state reset. - Check for validation errors —
document.querySelector('.validation-summary-errors, .text-danger')?→ if present, read the error text. - Check browser console —
console.clear()thenconsole.log('ready')before action, check for JS errors after. - Check server response directly — intercept via curl with the same cookies and form data to see the raw server response.
- Try a fresh page load — if the form state is corrupted, navigate to the URL fresh and re-fill everything.