How to Test Forgot Password on Web (Complete Guide)
Forgot‑password functionality is one of the few user‑facing paths that directly touches credential recovery, yet it is often treated as an after‑thought in test plans. When it fails, users cannot rega
Why Forgot Password Testing Matters
Forgot‑password functionality is one of the few user‑facing paths that directly touches credential recovery, yet it is often treated as an after‑thought in test plans. When it fails, users cannot regain access to their accounts, support tickets spike, and trust erodes. From a security standpoint, a weak reset flow can become an entry point for account takeover, credential stuffing, or information leakage. In production, subtle bugs—such as a token that lives too long, a rate‑limit that is too permissive, or an email that lands in spam—only surface under real‑world load or with specific user personas. A thorough test strategy therefore protects both usability and the integrity of the authentication system.
Forgot Password Flow Overview
Most web implementations follow a similar pattern, though details vary by product and compliance requirements.
Typical Steps
- Entry point – user clicks a “Forgot password?” link on the login page.
- Identification – user supplies an identifier (email, username, or phone).
- Validation – backend checks whether the identifier exists and is eligible for reset (not locked, not deactivated).
- Token generation – a cryptographically random, single‑use token is created and stored with an expiry timestamp.
- Out‑of‑band delivery – token is sent via email, SMS, or a push notification.
- User consumption – user clicks the link or enters the token in a reset form.
- Password update – user provides a new password that meets policy; backend validates token, updates credential hash, and invalidates any existing sessions.
- Confirmation – user sees a success message and is redirected to login or a post‑reset landing page.
Variations
- Security questions – some systems ask for pre‑registered answers instead of sending a token.
- One‑time code – a numeric code is delivered; user types it into a verification screen.
- Magic link – the email contains a URL that automatically logs the user in and prompts for a new password.
- Social‑login fallback – if the account is linked to an identity provider, the reset may trigger a provider‑initiated flow.
Backend Considerations
- Token entropy (≥128 bits) and storage (hashed token, not plaintext).
- Expiry window (commonly 15 min to 1 h).
- Rate limiting per identifier and per IP address.
- Logging that avoids leaking whether an identifier exists (use generic messages).
- Integration with external mail providers (SMTP, SendGrid, SES) and handling of bounce or delay events.
Understanding these pieces helps you map test conditions to concrete implementation details.
Comprehensive Test Matrix
Below is a granular matrix that covers the functional, error, edge, accessibility, and security dimensions of a forgot‑password flow. Each row can be turned into a test case; the columns indicate the suggested priority (P0 = blocking, P1 = high, P2 = medium) and the typical severity if the defect reaches production.
| # | Test Category | Description | Priority | Expected Result | Failure Severity |
|---|---|---|---|---|---|
| 1 | Happy Path – Email | Submit valid registered email → receive reset link → click → set new password → login with new creds | P0 | Success message, able to login | Critical |
| 2 | Happy Path – Username | Same as 1 but using username as identifier | P0 | Same as email path | Critical |
| 3 | Happy Path – Phone/SMS | Submit valid phone number → receive OTP → enter OTP → set new password → login | P0 | Success | Critical |
| 4 | Error – Non‑existent ID | Submit email/username that is not in the system | P1 | Generic message (“If the account exists, you will receive…”) – no enumeration | Medium |
| 5 | Error – Malformed Input | Submit email without @, extremely long string, SQL‑injection payload | P1 | Client‑side validation error or server‑side 400 with generic message | Low |
| 6 | Error – Already Reset | Submit identifier for an account that already has a pending reset token (token not yet expired) | P1 | System either rejects with “A reset request is already in progress” or silently ignores and sends a new token (both acceptable if documented) | Low |
| 7 | Edge – Token Expiry | Request reset, wait > expiry time, then attempt to use the link | P1 | System shows “Link has expired or is invalid” | Medium |
| 8 | Edge – Token Reuse | Use the same token twice to reset password | P1 | Second attempt fails with invalid token | Medium |
| 9 | Edge – Concurrent Requests | Trigger two reset requests in quick succession for same ID | P2 | Either both succeed with distinct tokens or second is throttled; no account lockout | Low |
| 10 | Edge – Rate Limit | Send N reset requests (N = configured limit +1) from same IP or identifier | P1 | Subsequent requests receive 429 or generic “Try again later” | Medium |
| 11 | Edge – Special Characters | Identifier contains Unicode, spaces, plus signs, or sub‑addressing (e.g., user+tag@example.com) | P2 | System accepts and delivers email correctly | Low |
| 12 | Accessibility – Keyboard Navigation | Tab through the forgot‑password form, activate submit with Enter, verify focus order and visible focus rings | P1 | All controls reachable and operable via keyboard | Medium |
| 13 | Accessibility – Screen Reader | Use NVDA/JAWS or VoiceOver to announce labels, error messages, and live regions | P1 | Meaningful announcements, ARIA‑labelled inputs, descriptive error text | Medium |
| 14 | Accessibility – Color Contrast | Verify WCAG AA contrast for text, buttons, and links on the reset page | P1 | Contrast ratio ≥4.5:1 (normal text) | Low |
| 15 | Security – Token Entropy | Inspect generated token (via test hook or logs) for length and randomness | P0 | Token ≥128 bits, cryptographically random (no predictable patterns) | Critical |
| 16 | Security – Token Storage | Verify that stored token is hashed (e.g., bcrypt) and not recoverable from DB dumps | P0 | Only hash stored; plaintext never appears in logs or responses | Critical |
| 17 | Security – Information Leakage | Attempt to enumerate valid accounts via response timing or message differences | P1 | No discernible difference in status code, timing, or message body between existing and non‑existing IDs | Medium |
| 18 | Security – Rate Limit Bypass | Try to reset using different identifiers but same IP, or via proxies, to see if limit is per‑IP only | P1 | Limit enforced per identifier *and* per IP (or as per policy) | Medium |
| 19 | Security – CSRF on Reset Form | Submit reset form without valid CSRF token | P1 | Request rejected (403) or requires token | Medium |
| 20 | Security – Clickjacking | Attempt to frame the reset page in an iframe from another origin | P1 | Page protected with X‑Frame‑Options: DENY or SAMEORIGIN, or CSP frame‑ancestors | Low |
| 21 | Privacy – Email Content | Verify that reset email contains only the token link, no personal data (e.g., username, last login) | P2 | Email body minimal, no PII beyond what is needed for the link | Low |
| 22 | Privacy – Logging | Ensure logs never capture the plain token or the new password | P1 | Logs show only hashed token or placeholder | Medium |
| 23 | I18n – Language Switch | Change UI language, go through reset flow, verify all text and placeholders translate correctly | P2 | All UI elements reflect selected language | Low |
| 24 | Compliance – Consent Check | If GDPR‑style consent is required for processing email, confirm that consent checkbox is enforced | P2 | Flow blocked until consent given | Low |
How to use the matrix
- Treat each row as a test case in your test management tool.
- Automate the happy path and the most critical error/edge cases (rows 1‑4, 7‑10, 15‑16, 18‑19).
- Run accessibility and security checks (rows 12‑14, 17, 20‑22) as part of your CI pipeline with specialized tooling (axe, OWASP ZAP).
- Reserve the remaining rows for exploratory or manual testing, especially those that depend on timing or external services (rows 8, 9, 11, 21‑24).
Severity/Priority Reference Table
| Severity | Description | Typical Fix Timeline |
|---|---|---|
| Critical | Blocks core authentication; enables account takeover or denial of service | Immediate (within 24 h) |
| High | Severely impacts usability or security but does not allow direct compromise | Within 3 sprints |
| Medium | Causes user frustration, support overhead, or minor policy deviation | Next release |
| Low | Cosmetic, rarely observed, or only relevant under exotic conditions | Backlog |
Manual Testing Forgot‑ Complete Guide
Manual Testing Approach
Manual Approach
Approach
A disciplined manual session can uncover subtle issues that automated scripts miss, especially when human perception of wording, visual layout, or timing is involved.
Environment Setup
- Browser profile – use a clean profile (no extensions, cache disabled) to avoid interference from password managers or ad‑blockers.
- Network throttling – simulate 3G or offline moments via DevTools → Network → throttling to see how the UI behaves when the email service is slow or unavailable.
- Mail capture – set up a local SMTP sink (e.g., MailHog, Papercut) or a test‑only mailbox (e.g., a Gmail alias with +test) to inspect the exact reset email without spamming real users.
- Backend hooks – if available, enable a test endpoint that returns the generated token or its hash, allowing you to verify expiry and storage without parsing emails.
- Accessibility tools – install axe core browser extension, and have a screen‑reader (NVDA on Windows, VoiceOver on macOS) ready for quick checks.
Step‑by‑Step Checklist
| Step | Action | Observation Points |
|---|---|---|
| 1 | Navigate to login page, locate “Forgot password?” link | Link visible, keyboard focusable, aria‑label descriptive |
| 2 | Click link → lands on reset request page | URL changes appropriately, no redirect loops |
| 3 | Enter a valid registered email | Field accepts input, shows inline validation if any |
| 4 | Submit form | Loading indicator appears, no page reload unless intended |
| 5 | Check mail capture | Email arrives within expected time (≤30 s), contains only token link, subject line generic |
| 6 | Click token link (or copy‑paste) | Browser navigates to reset password page, token appears in URL or hidden field |
| 7 | Enter new password meeting policy | Password strength meter updates correctly, error messages for policy violations |
| 8 | Submit new password | Success toast/message, session invalidated (you are logged out) |
| 9 | Attempt login with old password | Login fails with “Invalid credentials” |
| 10 | Login with new password | Access granted, redirected to expected post‑login page |
| 11 | Repeat steps using username/phone as identifier | Same success flow |
| 12 | Try submitting non‑existent ID | Generic message, no indication whether account exists |
| 13 | Attempt to submit malformed email (e.g., “test@@example”) | Inline validation error or server 400 with generic reply |
| 14 | Trigger multiple rapid requests (e.g., via curl) | Observe rate‑limit responses (429) or generic throttling |
| 15 | Use screen reader to navigate the form | All labels announced, live region updates for success/error |
| 16 | Disable CSS or use high‑contrast mode | Ensure readability, focus outlines visible |
| 17 | Change browser language, repeat flow | All static text translates, placeholders update |
| 18 | Inspect network requests | Verify token is sent over HTTPS only, no token in query string of subsequent requests after reset |
| 19 | Check DevTools → Application → Storage | Confirm that any temporary token stored in localStorage/sessionStorage is cleared after reset |
| 20 | Review server‑side logs (if accessible) | Ensure no plain token or password appears; only hashed token or audit entry |
Common Pitfalls to Spot
- Token leakage via Referer header – if the reset link is clicked from an external email client, the token may appear in the Referer when loading third‑party resources. Ensure the reset page strips sensitive query params or uses POST‑based token verification.
- UI shows “Invalid token” but actually accepts it – a mismatch between front‑end validation and back‑end logic can cause confusion; verify both ends agree.
- Email client strips link – some corporate mail systems rewrite URLs for security testing; test with a variety of providers (Gmail, Outlook, Yahoo).
- Fallback to security questions – if the system offers a secret question after failed email delivery, ensure that the question is not guessable and that the answer is stored securely.
- Locale‑specific validation – certain locales treat the plus sign in email addresses differently; confirm that your regex permits it if you intend to support sub‑addressing.
By following this checklist, you gain confidence that the flow works for the majority of users while also catching the subtle defects that only a human eye (or a well‑crafted exploratory session) can notice.
Automated Testing Approaches
Automation provides repeatability and fast feedback, especially for regression and continuous integration. Below are strategies tailored to the web forgot‑password flow.
Unit / API Tests for Backend
If your application exposes a dedicated API endpoint for password reset (e.g., POST /api/v1/auth/forgot), test it directly:
// Example using Jest and supertest
const request = require('supertest');
const app = require('../src/app'); // express instance
describe('Forgot password API', () => {
it('returns 200 for known email', async () => {
const res = await request(app)
.post('/api/v1/auth/forgot')
.send({ email: 'user@example.com' })
.expect(200);
expect(res.body.message).toMatch(/check your inbox/i);
});
it('does not reveal account existence', async () => {
const res1 = await request(app)
.post('/api/v1/auth/forgot')
.send({ email: 'known@example.com' })
.expect(200);
const res2 = await request(app)
.post('/api/v1/auth/forgot')
.send({ email: 'unknown@nonexistent.com' })
.expect(200);
// Both responses should be indistinguishable
expect(res1.text).toEqual(res2.text);
});
it('enforces rate limit per IP', async () => {
for (let i = 0; i < 5; i++) {
await request(app)
.post('/api/v1/auth/forgot')
.send({ email: `test${i}@example.com` })
.expect(200);
}
const res = await request(app)
.post('/api/v1/auth/forgot')
.send({ email: 'ratelimit@example.com' })
.expect(429);
expect(res.body.error).toMatch(/too many requests/i);
});
});
These tests validate business logic without launching a browser, making them fast and reliable for CI.
UI Tests with Selenium / WebDriver
When you need to assert DOM changes, visual cues, or email delivery, a UI test is appropriate. Below is a Python‑ Selenium example that uses a test mailbox (MailHog) to capture the reset link.
import time
import re
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import requests
MAILHOG_API = "http://localhost:8025/api/v2/messages"
def get_latest_email():
resp = requests.get(MAILHOG_API).json()
items = resp.get('items', [])
if not items:
return None
latest = items[0]
# Assume the email contains a link with token
body = latest['Content']['Body']
match = re.search(r'https?://[^\s"]+reset[^\s"]+', body)
return match.group(0) if match else None
def test_forgot_password_flow():
driver = webdriver.Chrome()
wait = WebDriverWait(driver, 15)
try:
driver.get("https://app.example.com/login")
# Click forgot password link
wait.until(EC.element_to_be_clickable((By.LINK_TEXT, "Forgot password?"))).click()
# Enter email
email_input = wait.until(EC.presence_of_element_located((By.NAME, "email")))
email_input.send_keys("qa-test@example.com")
email_input.submit()
# Wait for success toast
wait.until(EC.visibility_of_element_located((By.CLASS_NAME, "toast-success")))
# Poll MailHog for the email (max 30s)
deadline = time.time() + 30
reset_url = None
while time.time() < deadline:
reset_url = get_latest_email()
if reset_url:
break
time.sleep(2)
assert reset_url, "Reset email not received"
# Open reset link
driver.get(reset_url)
# Set new password
pwd_input = wait.until(EC.presence_of_element_located((By.NAME, "password")))
pwd_input.send_keys("NewP@ssw0rd!")
confirm_input = driver.find_element(By.NAME, "password_confirm")
confirm_input.send_keys("NewP@ssw0rd!")
driver.find_element(By.XPATH, "//button[@type='submit']").click()
# Expect success message
wait.until(EC.text_to_be_present_in_element((By.TAG_NAME, "p"),
"Password updated"))
# Logout and login with new creds
driver.get("https://app.example.com/logout")
driver.get("https://app.example.com/login")
wait.until(EC.presence_of_element_located((By.NAME, "email"))).send_keys("qa-test@example.com")
driver.find_element(By.NAME, "password").send_keys("NewP@ssw0rd!")
driver.find_element(By.XPATH, "//button[@type='submit']").click()
assert "Dashboard" in driver.title
finally:
driver.quit()
Why this works
- The test validates end‑to‑end navigation, email receipt, token consumption, and credential update.
- Using a disposable mailbox avoids polluting real inboxes and gives deterministic access to the token.
- Explicit waits (
WebDriverWait) mitigate flakiness caused by network latency or slow email delivery.
Cypress Example
Cypress excels at testing within the same origin; for cross‑origin email handling you can stub the mail request or use a third‑party service like Mailosaur.
// cypress/integrations/forgot_password.spec.js
describe('Forgot password flow', () => {
const testEmail = 'cypress-test@example.com';
beforeEach(() => {
cy.visit('/login');
cy.get('a[href="/forgot"]').click();
});
it('shows success message after submitting known email', () => {
cy.get('input[name="email"]').type(testEmail);
cy.get('button[type="submit"]').click();
cy.contains(/if the account exists, you will receive an email/i)
.should('be.visible');
});
it('allows password reset via email link (using Mailosaur)', () => {
// Assume Mailosaur provides an API to fetch the latest email
cy.task('getResetLink', { email: testEmail }).then((link) => {
expect(link).to.be.a('string');
cy.visit(link);
cy.get('input[name="password"]').type('NewP@ssw0rd!{enter}');
cy.get('input[name="password_confirm"]').type('NewP@ssw0rd!{enter}');
cy.contains('Password updated successfully').should('be.visible');
// Logout and login with new creds
cy.clearCookies();
cy.visit('/login');
cy.get('input[name="email"]').type(testEmail);
cy.get('input[name="password"]').type('NewP@ssw0rd!{enter}');
cy.url().should('include', '/dashboard');
});
});
});
The cy.task command would call a Node script that queries Mailosaur’s REST API for the latest message matching the sender/subject and extracts the magic link.
Playwright Example (TypeScript)
Playwright offers built‑in network interception and multiple browser contexts, useful for simulating rate limits.
// tests/forgot-password.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Forgot password', () => {
test('rate limit blocks after N attempts', async ({ page }) => {
await page.goto('/login');
await page.click('text=Forgot password?');
const emailInput = page.locator('input[name="email"]');
const submitBtn = page.locator('button[type="submit"]');
// Simulate 5 rapid requests (limit = 4)
for (let i = 0; i < 5; i++) {
await emailInput.fill(`test${i}@example.com`);
await submitBtn.click();
// Wait for either toast or error
await page.waitForTimeout(500);
}
const lastResponse = await page.locator('.toast-error').textContent();
expect(lastResponse?.toLowerCase()).toContain('too many requests');
});
test('token expires after set time', async ({ page }) => {
// ... }) => {
// request reset
await page.goto('/login');
await page.click('text=Forgot password?');
await page.fill('input[name="email"]', 'user@example.com');
await page.click('button[type="submit"]');
// intercept the email sending endpoint to capture token
let token: string | null = null;
await page.route('**/api/v1/auth/forgot', async route => {
const resp = await route.fetch();
const json = await resp.json();
token = json.token; // assuming test backend returns token
await route.fulfill({ resp });
});
// wait for token capture
await page.waitForTimeout(1000);
expect(token).not.toBeNull();
// simulate waiting past expiry (e.g., 10 min) using fake timer
// if your app uses JS Date, you can override via page.evaluate
await page.evaluate(() => {
// shift system time forward by 11 minutes
const originalDate = Date;
// @ts-ignore
Date = class extends originalDate {
constructor() { super(); }
now() { return super.now() + 11 * 60 * 1000; }
};
});
// now try to use the token link
await page.goto(`/reset-password?token=${token}`);
await page.fill('input[name="password"]', 'NewP@ssw0rd!');
await page.fill('input[name="password_confirm"]', 'NewP@ssw0rd!');
await page.click('button[type="submit"]');
const errorMsg = await page.locator('.form-error').textContent();
expect(errorMsg?.toLowerCase()).toContain('expired or invalid');
});
});
});
Key takeaways for automation
- Separate concerns: unit/API tests for logic, UI tests for flow and presentation.
- Use disposable mail services or test hooks to avoid reliance on external SMTP.
- Parameterize tests with data‑driven sources (CSV, JSON) to cover multiple identifiers and locales.
- Integrate accessibility checks (axe‑core) and security scans (OWASP ZAP) as separate pipeline stages.
- Monitor flakiness: replace
sleep‑like waits with explicit conditions, and run UI tests in parallel with isolated browser contexts.
Tooling and Frameworks Specific to Web
Beyond generic Selenium/Cypress/Playwright, several niche tools add value when testing forgot‑password flows.
| Category | Tool | What it adds for forgot‑password | Example usage | |
|---|---|---|---|---|
| Accessibility | axe‑core (npm, Chrome/Firefox extension) | Automated WCAG scans on the reset request and reset password pages; can be run in CI via axe-playwright or axe-selenium. | npx axe-playwright ./tests/forgot_password.spec.js --tags wcag2aa | |
| Security | OWASP ZAP (Docker or desktop) | Active scanner that can probe for CSRF, information leakage, and weak token generation; includes an API to automate scans in CI. | zap-baseline.py -t https://app.example.com/forgot -r zap-report.html | |
| Email Testing | MailHog, Mailosaur, Etherea | Captures outbound email, provides API to fetch latest message, extract links, and assert content. | `curl http://localhost:8025/api/v2/messages | jq '.items[0].Content.Body'` |
| Token Inspection | JWT Decoder (if tokens are JWT) or custom test endpoint | Allows you to verify token claims (expiry, audience) without relying on email delivery. | Add a dev endpoint GET /debug/token/:id that returns decoded payload (only in test env). | |
| Performance | k6 or Artillery | Simulates burst of reset requests to validate rate‑limiting and backend throughput under load. | k6 run --vus 50 --duration 2m script.js where script hits /forgot endpoint. | |
| Visual Regression | Applitools, Percy | Detects unintended UI shifts on the reset pages after a CSS or layout change. | applitools eyes.open(driver, "Forgot Password", "reset page") | |
| Test Data Management | Factory Boy (Python) or fixture‑factory (JS) | Generates realistic user records with varied email formats, ensuring you test edge cases like plus‑addressing or international domains. | UserFactory.create_batch(10, email=fake.unique().safe_email()) |
Integrating these tools into a CI pipeline creates a safety net that catches regressions before they reach production. For instance, a typical pipeline could be:
- Unit tests (Jest/Mocha) – run on every commit.
- Static security scan (ZAP baseline) – run nightly.
- UI test suite (Playwright) – run on PRs against a preview environment.
- Accessibility scan (axe) – run on UI test completion.
- Load test (k6) – run weekly against staging.
- Email verification (MailHog) – embedded in UI test steps.
When any stage fails, the build is blocked, and the responsible team receives a detailed report.
Autonomous, Persona‑Driven Exploration with SUSA
While scripted tests cover the happy path and known edge cases, real users exhibit a wide variety of behaviors that static scripts never anticipate. An autonomous QA platform like SUSA can explore the application without pre‑written steps, using simulated personas to surface hidden defects.
How SUSA Discovers Forgot Password Flows
- Crawl & Map – Upon receiving the web URL, SUSA builds a graph of reachable states by interacting with links, buttons, and form fields. The “Forgot password?” link is treated like any other UI element; if it is present, the crawler follows it.
- Persona Injection – Each explored state is exercised by a set of virtual users, each embodying a distinct behavior profile:
- Curious – tries every alternative input (e.g., enters phone number in email field, pastes emojis).
- Impatient – submits forms rapidly, triggers multiple requests in quick succession.
- Novice – relies heavily on placeholders, may miss error messages if they disappear quickly.
- Adversarial – attempts SQL injection, XSS payloads, and oversized strings in the identifier field.
- Elderly – prefers larger click targets, may use keyboard navigation exclusively.
- Accessibility – enables screen‑reader mode, high contrast, and disables CSS to verify fallback experiences.
- Power user – uses keyboard shortcuts, browser autofill, and attempts to reuse old tokens.
- Observation & Assertion – SUSA monitors HTTP responses, DOM mutations, console errors, and network timings. It automatically checks for:
- Presence of generic messages that avoid account enumeration.
- Correct HTTP status codes (200, 420, 429).
- Token validity (expiry, single‑use) via a test hook if available.
- Accessibility violations (missing labels, insufficient contrast).
- Security red flags (token in URL, lack of CSP, missing X‑Frame‑Options).
- Learning Loop – After each run, SUSA remembers which screens led to dead ends (e.g., a form that never shows a success message) and which inputs caused errors. Subsequent runs prioritize unexplored paths, increasing coverage over time.
What SUSA Finds That Scripts Miss
- Hidden alternative entry points – Some apps expose a “Forgot password?” link inside a modal that only appears after a failed login attempt. Scripts that start from the login page may never see it unless they simulate the failure first.
- Dynamic language switching – A persona that changes the UI language mid‑flow can expose missing translations in the reset email subject line, something a static test that fixes a locale would overlook.
- Timing‑sensitive UI cues – An impatient persona may click the submit button before the loading spinner disappears, revealing a race condition where the form accepts duplicate submissions.
- Adversarial payloads – By trying strings like
' OR 1=1--orin the email field, SUSA can uncover insufficient sanitization that leads to reflected XSS or SQL errors bubbling up to the UI. - Accessibility fallback – When the accessibility persona disables JavaScript, SUSA can verify that the reset form still functions via a classic POST (progressive enhancement) or that a clear noscript notice is present.
Example Output (condensed)
Run #42 – Persona: Impatient
- Submitted reset request 3 times within 800ms.
- Backend responded with 429 on the 2nd attempt (as expected).
- UI showed generic “Please wait” toast but did not disable the submit button,
allowing a 4th click that resulted in a 500 Internal Server Error.
→ Logged: duplicate submission race condition.
Run #57 – Persona: Adversarial
- Entered email: `test@example.com'<script>alert('xss')</script>`
- Response contained the unsanitized string in the error message:
“Invalid email: test@example.com'<script>alert('xss')</script>”.
→ XSS vector identified (CWE‑79).
Run #68 – Persona: Elderly (keyboard only)
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free