Post

BugForge - Weekly - Fur Hire

BugForge - Weekly - Fur Hire

Weekly - FurHire


Vulnerabilities Covered:
WAF Bypass via Obscure Event Handler (XSS)
Cross-Site Request Forgery (CSRF) - Password Takeover

Summary:
This walkthrough demonstrates a chained attack against a job recruitment application protected by a WAF. The application reflects unsanitised input in the application status field, but the WAF blocks common XSS payloads and event handlers such as onerror and alert. By switching to the lesser-known oncontentvisibilityautostatechange event handler on an <img> tag, the WAF filter is bypassed entirely. With XSS execution confirmed, cookie theft is ruled out because the session cookie carries the HttpOnly flag. Instead, the password change endpoint is found to require neither a CSRF token nor the current password, and it honours same-origin fetch requests. A base64-encoded fetch payload is injected into the status field, which silently changes the job seeker’s password when they view an accepted application notification. Logging in with the new password retrieves the flag.

Reference:
Bugforge.io
PortSwigger - XSS Cheat Sheet


Solution

Step 1 - Application Analysis

The application is a job recruitment platform with two distinct roles: job seeker and recruiter. To map the attack surface, two accounts were created - one job seeker and one recruiter. The recruiter posts a job listing and the job seeker applies for it.

After the recruiter accepts the application, a notification is sent to the job seeker. This notification path is the key: whatever the recruiter writes in the status field when accepting an application is reflected back to the job seeker. This is the candidate injection point.

Source code review


Step 2 - Finding the Injection Point

The application status is set via a PUT request to /api/applications/:id/status. Testing with a basic HTML tag confirms that the value is rendered in the browser without sanitisation - the response reflects the raw HTML.

Initial XSS attempts using common event handlers are immediately blocked.

WAF blocking XSS


Step 3 - WAF Analysis

With basic XSS confirmed as blocked, the WAF rules were probed systematically. Common event handlers such as onerror, onload, and onclick are all blocked. JavaScript function calls like alert() are also filtered.

Alert blocked by WAF

The WAF appears to maintain a blocklist of well-known event handlers. The strategy is to find an event handler that is valid in modern browsers but not present in the WAF’s blocklist.


Step 4 - WAF Bypass via Obscure Event Handler

Consulting the PortSwigger XSS cheat sheet surfaced oncontentvisibilityautostatechange, a relatively new event that fires when an element’s content visibility state changes. This event handler is not in the WAF’s blocklist.

oncontentvisibilityautostatechange attribute

The handler requires style=display:block;content-visibility:auto on the element to trigger automatically. Since alert is blocked by the WAF, execution was confirmed using both print() and a blind XSS payload pointing to an out-of-band callback server.

1
<img oncontentvisibilityautostatechange=print() style=display:block;content-visibility:auto>

WAF bypass with attribute payload

The print dialog fires when the job seeker views the application, confirming client-side JavaScript execution through the WAF.

Print dialog triggered

With bypass confirmed, a blind XSS payload was injected into the application status field to verify execution in the job seeker’s browser context via an out-of-band callback.

Blind XSS payload to confirm job seeker-side execution

Website Hook - Request


The natural next step after confirming XSS is to attempt to steal the session cookie. Inspecting the job seeker’s session cookie reveals it is flagged as HttpOnly, meaning it is not accessible to JavaScript.

HttpOnly cookie flag

Cookie theft is not viable. A different impact vector is needed.


Step 6 - CSRF via Password Change

Inspecting the application for sensitive state-changing endpoints, the password update endpoint was identified: PUT /api/profile/password. Testing the endpoint revealed:

  • No CSRF token is required
  • The current password is not required
  • The endpoint accepts a newPassword field in the JSON body
  • Same-origin fetch requests from XSS are honoured

Password update request structure

This means the XSS payload can silently call the password change endpoint on behalf of the job seeker, setting a known password and enabling account takeover.

To bypass the WAF’s filtering of the JSON body, the payload is base64-encoded and decoded at runtime using atob().

The JSON payload {"newPassword":"Zwarts"} encodes to eyJuZXdQYXNzd29yZCI6Ilp3YXJ0cyJ9.

Base64 encoded payload

The final exploit payload:

1
<img oncontentvisibilityautostatechange=fetch('/api/profile/password',{'method':'PUT','headers':{'Content-Type':'application/json'},'body':atob('eyJuZXdQYXNzd29yZCI6Ilp3YXJ0cyJ9')}) style=display:block;content-visibility:auto>

Full password update payload


Step 7 - Triggering the Attack

The malicious payload is submitted as the application status when accepting Jeremy’s (the job seeker’s) application. When Jeremy views the accepted application notification, the XSS fires in their browser context and silently issues the password change request.

XSS attack via Jeremy's application acceptance

Logging in as Jeremy with the new password password2 succeeds, confirming the account takeover and retrieving the flag.

Flag


Impact

  • Full account takeover of a job seeker account through a blind XSS triggered by a job application, with no interaction required beyond the job seeker viewing a notification
  • The WAF provides a false sense of security - a single unlisted event handler is sufficient to bypass it entirely
  • The absence of CSRF tokens and current-password verification on the password change endpoint turns reflected XSS into an account takeover primitive
  • HttpOnly cookies alone are insufficient protection when CSRF on sensitive endpoints is left unaddressed
  • An attacker with job seeker access gains visibility over all applications, candidate data, and any privileged actions available to that role

Vulnerability Classification

WAF Bypass (XSS)

  • OWASP Top 10: A03:2021 - Injection
  • Vulnerability Type: Stored XSS with WAF Bypass via Obscure Event Handler
  • Attack Surface: PUT /api/applications/:id/status - status field reflected to job seeker and job seeker without sanitisation
  • CWE:
    • CWE-79 - Improper Neutralisation of Input During Web Page Generation (XSS)
    • CWE-693 - Protection Mechanism Failure

CSRF - Password Takeover

  • OWASP Top 10: A01:2021 - Broken Access Control
  • Vulnerability Type: Cross-Site Request Forgery on Password Change Endpoint
  • Attack Surface: PUT /api/profile/password - no CSRF token, no current password verification
  • CWE:
    • CWE-352 - Cross-Site Request Forgery
    • CWE-620 - Unverified Password Change

Root Cause

WAF Bypass: The WAF relies on a blocklist of known-bad patterns. Blocklist-based filters are fundamentally incomplete - any event handler or JavaScript construct not present in the list passes through unchecked. The root cause is trusting a perimeter control rather than fixing the underlying output encoding. The status field is stored and rendered as raw HTML, which is the real vulnerability. The WAF only delays exploitation.

CSRF: The password change endpoint performs no origin verification beyond relying on the browser’s same-origin policy for cookies. Because there is no CSRF token and no requirement to supply the current password, any JavaScript running in the user’s browser context can issue an authenticated password change. When combined with XSS, this collapses account security to a single injection point.


Remediation

XSS / WAF Bypass:

  • Fix the root cause: HTML-encode all user-supplied output before rendering it in the browser; do not rely on the WAF as the primary defence
  • Use a Content Security Policy (CSP) to restrict which scripts and event handlers can execute, reducing the impact of any bypass
  • Transition the WAF from a blocklist to an allowlist model where only explicitly permitted patterns are accepted
  • Validate and sanitise the status field server-side to a set of known safe values (e.g., accepted, rejected) rather than accepting freeform HTML

CSRF - Password Change:

  • Require the user’s current password as part of any password change request, ensuring the requester possesses the credential
  • Add a per-session CSRF token to all state-changing endpoints and validate it server-side on every request
  • Set the session cookie’s SameSite attribute to Strict or Lax to prevent cross-origin requests from carrying credentials
  • Audit all sensitive endpoints (email change, password reset, role update) for the same missing CSRF protection

This post is licensed under CC BY 4.0 by the author.