FormBucket is convenient because it lets you collect form submissions without building a full backend. That convenience also creates a trap: teams treat it like a harmless inbox, then start rendering submission data in dashboards, emails, admin tools, thank-you pages, or internal review apps. That’s where XSS shows up.
The problem usually isn’t FormBucket itself. The problem is what developers do with untrusted form data after it lands.
If you accept name, message, company, or notes from a public form, assume every field is attacker-controlled HTML and JavaScript. If you forget that even once, you get stored XSS, reflected XSS, or DOM-based XSS depending on how you display it.
Here are the mistakes I see most often and how I’d fix them.
Mistake #1: Treating form submissions as “safe because they came from our form”
This is the classic bad assumption.
A form field in your UI might look harmless:
<input name="name" />
<textarea name="message"></textarea>
But an attacker doesn’t need to use your UI. They can submit directly with their own request:
curl -X POST https://your-form-endpoint.example \
-d 'name=<script>alert(1)</script>' \
-d 'message=<img src=x onerror=alert(2)>'
If that payload later gets rendered in an admin panel, support dashboard, or confirmation page without escaping, you’ve got XSS.
Fix
Treat every FormBucket field as untrusted input forever. Validation helps, but output encoding is what actually stops XSS.
For example, server-side rendering with proper escaping:
function escapeHtml(value) {
return String(value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
app.get('/submissions/:id', async (req, res) => {
const submission = await getSubmission(req.params.id);
res.send(`
<h1>Submission</h1>
<p><strong>Name:</strong> ${escapeHtml(submission.name)}</p>
<p><strong>Message:</strong> ${escapeHtml(submission.message)}</p>
`);
});
Even better, use a templating engine that escapes by default and don’t bypass it.
Mistake #2: Rendering submission data with innerHTML
I still see this in internal tools all the time. A team fetches FormBucket submissions and drops them into the page:
results.innerHTML = `
<div class="entry">
<h2>${submission.name}</h2>
<p>${submission.message}</p>
</div>
`;
That’s an XSS sink. If submission.message contains HTML, the browser parses it.
Fix
Use textContent for plain text. Most form fields should be plain text.
const entry = document.createElement('div');
entry.className = 'entry';
const title = document.createElement('h2');
title.textContent = submission.name;
const message = document.createElement('p');
message.textContent = submission.message;
entry.appendChild(title);
entry.appendChild(message);
results.appendChild(entry);
If you absolutely need to allow some HTML formatting, sanitize it with a well-maintained HTML sanitizer and define an allowlist of tags and attributes. Don’t roll your own sanitizer. I’ve watched people try that. It goes badly.
Mistake #3: Escaping input on arrival instead of encoding on output
A lot of developers try to “sanitize once” when the form is submitted:
const cleanName = req.body.name.replace(/</g, '<');
saveSubmission({ name: cleanName });
This sounds tidy, but it causes two problems:
- It’s incomplete and easy to bypass.
- It mixes storage with presentation.
The same data might later be used in HTML, an attribute, JavaScript, JSON, CSV export, or email. Each context has different encoding rules.
Fix
Store the raw value, validate it for business rules, and encode it at the point where you render it.
A good pattern looks like this:
function validateSubmission(body) {
return {
name: String(body.name || '').slice(0, 100),
email: String(body.email || '').slice(0, 254),
message: String(body.message || '').slice(0, 5000),
};
}
app.post('/submit', async (req, res) => {
const submission = validateSubmission(req.body);
await saveSubmission(submission);
res.status(204).end();
});
Then escape based on output context later.
Mistake #4: Forgetting that attributes are a different output context
Developers often escape text nodes but forget attributes:
<div data-name="{{ submission.name }}"></div>
If your templating system doesn’t correctly escape attribute values, attacker-controlled quotes can break out and inject new attributes or event handlers.
This gets worse when someone builds links from form data:
profileLink.innerHTML = `<a href="${submission.website}">Website</a>`;
Now you’re dealing with both HTML injection and dangerous URL schemes like javascript:.
Fix
Use safe DOM APIs and validate URLs strictly.
function safeUrl(input) {
try {
const url = new URL(input);
if (url.protocol === 'http:' || url.protocol === 'https:') {
return url.toString();
}
} catch (_) {}
return null;
}
const link = document.createElement('a');
link.textContent = 'Website';
const url = safeUrl(submission.website);
if (url) {
link.href = url;
} else {
link.removeAttribute('href');
}
For server-rendered templates, rely on framework auto-escaping and avoid injecting raw attribute strings manually.
Mistake #5: Using “rich text” form fields without a real sanitizer
Sometimes product asks for formatted messages, bios, or support notes. Then somebody decides to allow HTML from FormBucket submissions.
That’s where things get ugly fast:
<script>tags- inline event handlers like
onerror - SVG payloads
javascript:URLs- malformed markup that bypasses naive filters
Fix
If you need rich text, sanitize with a strict allowlist before rendering. Keep the allowed surface area tiny.
Example with a sanitizer in Node:
import sanitizeHtml from 'sanitize-html';
function sanitizeRichText(html) {
return sanitizeHtml(html, {
allowedTags: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li', 'a'],
allowedAttributes: {
a: ['href']
},
allowedSchemes: ['http', 'https', 'mailto']
});
}
Then render only sanitized output in the specific places where rich text is expected. Don’t use this for normal fields like name or company. Those should stay plain text.
Mistake #6: Ignoring DOM-based XSS in client-side filtering and search
A lot of FormBucket dashboards are JavaScript-heavy. Developers fetch submission data, then build search results, previews, and filter chips in the browser. That often leads to DOM XSS.
Example:
const query = new URLSearchParams(location.search).get('q');
searchLabel.innerHTML = `Results for: ${query}`;
That’s reflected DOM XSS, and it has nothing to do with whether the submission itself was malicious.
Fix
Use textContent for user-controlled values from URLs, form fields, local storage, or API responses.
const query = new URLSearchParams(location.search).get('q') || '';
searchLabel.textContent = `Results for: ${query}`;
My rule is simple: if I’m touching untrusted data in the browser, I assume innerHTML is guilty until proven innocent.
Mistake #7: No Content Security Policy as a backstop
CSP won’t fix bad rendering logic, but it can make XSS harder to exploit. If someone slips an injection into a FormBucket-powered admin page, a solid CSP can stop inline scripts and unauthorized script sources from executing.
Too many teams skip this because they think CSP is optional. I don’t.
Fix
Start with a strict policy and adjust carefully:
Content-Security-Policy:
default-src 'self';
script-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
If your app needs nonces or hashes for scripts, use those instead of falling back to unsafe-inline.
For implementation patterns and CSP tuning details, see https://csp-guide.com.
Also review your framework or platform documentation for the official way to set CSP headers.
Mistake #8: Trusting internal admin tools less carefully than public pages
This one bites experienced teams. They protect the public marketing site, then build a quick internal moderation panel for FormBucket submissions with sloppy rendering because “only staff can access it.”
Stored XSS in internal tools is often worse than public-page XSS. If an attacker gets JavaScript running in an admin session, they may be able to read sensitive data, perform actions as staff, or pivot deeper into your environment.
Fix
Apply the same standards everywhere:
- escape output
- avoid
innerHTML - sanitize allowed HTML
- validate URLs
- deploy CSP
- review dangerous sinks like
eval,new Function, and inline event handlers
Internal does not mean safe. It just means the blast radius is bigger.
A safer pattern for FormBucket integrations
If I were building a FormBucket workflow today, I’d keep it boring:
- Accept all submission fields as untrusted.
- Validate type, length, and format on receipt.
- Store raw values.
- Render plain text with auto-escaping by default.
- Sanitize only the few fields that genuinely require limited HTML.
- Use safe DOM APIs in client-side code.
- Set a strict CSP.
- Test with payloads like:
<script>alert(1)</script><img src=x onerror=alert(1)>"><svg onload=alert(1)>javascript:alert(1)
That last step matters. I like to test the actual submission flow end to end: submit payload, open dashboard, export data, view confirmation page, check email templates. XSS often hides in the secondary features nobody thought about during implementation.
FormBucket makes data collection easy. Safe rendering is still your job. If you remember one thing, make it this: form submissions are not content, they are untrusted input wearing a friendlier outfit.