
// TL;DR
I found a stored XSS that fires through the submission side of a form builder: an attacker fills out someone else’s form with a payload in a normal field, and when the form’s owner later opens their submissions, the script executes in their session. That’s a low-privilege-attacks-high-privilege stored XSS leading to account takeover. The fields were filtered, so it wasn’t a one-shot
The idea: attack through the submission
Most people testing a form builder think about the form owner as the one entering data. But a form has two sides: the person who builds it, and the (often anonymous) people who submit it. The submission side is attacker-controlled input that gets stored and then displayed back — to the form owner — in their authenticated dashboard. That’s a textbook stored-XSS setup, and it’s easy to overlook because the payload travels in the “wrong” direction: from a low-privilege submitter up to a high-privilege account holder.
The attack scenario
The overall flow is straightforward:
- A victim creates a form — a survey, a contact form, anything — and shares its URL.
- The attacker opens that public form URL.
- In a normal field (for example, the Name field), the attacker enters a payload instead of a real value, and submits the form.
- Later, the victim logs into their account and opens the form’s entries / submissions to read what came in.
- At that moment the stored payload executes — in the victim’s authenticated session, in the builder’s own origin.
From there, the usual stored-XSS consequences follow, up to full account takeover of the form owner.
Getting it to fire wasn’t one-shot
The scenario is simple to describe, but landing the actual execution took iteration — it wasn’t a case of dropping <script>alert(1)> into a field and watching it pop. The submission fields had filtering in place, so a plain script tag did nothing, and I worked through a range of payloads before one stuck:
- Anchor /
href-based payloads — trying to sneak ajavascript:scheme into a link. img-based payloads — an<img>tag with a failingsrc.- Event-handler payloads —
onmouseover,onerror, and friends, which execute without the wordscriptappearing at all.
Rotating through these — and watching exactly how each one came back in the rendered submission — is what eventually got execution. The practical point: don’t give up on a field because <script> gets filtered. The event-handler and attribute-based variants are what carry most real-world XSS.
A tip that saves time here: before reaching for full XSS payloads, test for plain HTML injection first. Submit something harmless like <b>test</b> or <h1>test</h1> and see whether it renders as bold/a heading when the owner views the entry. If raw HTML renders, you know the field reflects markup unencoded, and you’ve confirmed the injection point exists — now it’s just a question of finding the payload that survives the filter. If it comes back escaped (as <b>test</b>), the field is probably safe and you can move on. HTML injection is the cheap probe that tells you whether XSS is even worth chasing.
Why it’s high impact
Two things push this above a typical reflected XSS:
- It crosses a privilege boundary. The attacker is an anonymous form-submitter — the lowest-privilege actor there is — yet the script runs in the context of the authenticated account owner. Low privilege attacking high privilege is exactly the pattern that earns higher severity.
- The victim triggers it doing something routine. Nobody has to be tricked into clicking a crafted link. The owner just reads their own submissions, which is the entire point of the product. The lure is the normal workflow.
Same bug, many sites
I reported it to Jotform first; their security team fixed it and rewarded a small bounty. Then I found the same pattern on a HackerOne Challenge (H1C) private site — the report was triaged and accepted, and rewarded with a $1000 bounty. After that, I kept finding the same class of bug across other sites too.


That’s the real lesson of this one: it isn’t a single vendor’s mistake, it’s a category. Any application where one user submits content that another user later views in an authenticated context is a candidate.
Takeaways
- Test the receiving end, not just the input end. On any multi-user feature — forms, tickets, reviews, messages — ask “who reads this later, and in what context?” The stored-XSS risk lives wherever attacker input is rendered back to a higher-privilege viewer.
- Probe with HTML injection first. Before burning time on XSS payloads, submit a harmless tag like
<b>test</b>and check whether it renders when the entry is viewed. If raw HTML renders, the injection point is confirmed and XSS is worth chasing; if it comes back escaped, move on. It’s the fastest way to tell a live field from a dead one. - Don’t quit when
<script>is filtered. A blocked script tag doesn’t mean the field is safe. Rotate through href-based, img-based, and event-handler payloads (onmouseover,onerror, …) — most real XSS lives in those variants, not in a bare<script>. - Mundane fields are still injection points. A “Name” field feels harmless. It’s still untrusted input that gets displayed somewhere, which is all XSS needs.
- A pattern that works once often works everywhere. Once you understand why a bug class exists, re-test the same idea across other targets — the same oversight repeats across products built the same way.
Thanks for reading — hope it was useful. Happy hacking!
Leave a Reply