For a long time, I pictured XSS in one very specific way: someone types malicious code into a form and that code later appears on a page.
That happens, but it is too narrow to explain why a frontend developer should care.
As soon as an interface reads a search term, URL parameter, API response, comment, browser preference or content from an external service, it is moving data from one place to another. The issue starts when one of those values is treated as browser instructions rather than text.
I am studying cybersecurity, and XSS is one of the topics that has made me rethink how I use JavaScript. Not because it involves some unusual trick, but because it brings me back to a simple question. What am I rendering, and which API am I using to do it?
What XSS means in frontend work
XSS stands for Cross-Site Scripting. It happens when a page executes content it should not execute because untrusted data is handled as HTML or JavaScript.
The outcome can be more serious than an alert on the screen. When someone has an active session, that code could change what they see, make requests in their name, capture data entered into forms or load resources from another site.
This is the part that matters to me as a frontend developer. The browser does not know whether content came from your team, an API or a search field. It interprets what it receives in the context you give it.
That means XSS does not necessarily begin in a form. It begins when a value you do not control reaches a place where the browser can read it as more than text.
Untrusted data shows up in more places than expected
A user comment is the standard example, but it is not the only one. In a web application, data worth treating carefully can come from different places
- A form.
- Your own API or a third-party API.
- URL parameters and fragments such as
location.hash. localStorage,sessionStorageor cookies that JavaScript can read.- Content written in a CMS.
- A JSON file.
- Analytics, chat services or any external script.
Data living in your own database is not automatically safe either. It could have arrived there through a form, an integration or a content migration.
I find this especially useful when thinking about content projects. In ICO Notes, for example, there is search, modules and a lot of information rendered on screen. I write the content today, but the question is still useful. If some of it came from another source tomorrow, would the code show it as text or try to interpret it as HTML?
It helps locate the points where the line between data and code can disappear.
The risk is usually in the way you render it
In JavaScript, innerHTML is convenient because it lets you build a piece of interface with a string.
results.innerHTML = `<p>Results for: ${query}</p>`;If query comes from a source you do not control, the browser will try to parse the resulting content as HTML. That is the problem.
If you only need to show text, there is no need to take that path. Create the node and assign text to it instead.
const message = document.createElement('p');
message.textContent = `Results for: ${query}`;
results.replaceChildren(message);When the value is text, textContent expresses that intention exactly. It displays a string rather than parsing it as markup.
The same pattern appears in card templates, search results, error messages, usernames and client-side previews. Where the value came from matters, but the place where you insert it determines how it should be handled.
Context matters as much as the value
The same value cannot be handled in the same way when it appears in a paragraph, an attribute, a URL or JavaScript code.
For example, building links from external data needs more than interpolating a string.
link.href = profileUrl;The browser accepting a value does not make it a suitable destination. Check that it is a URL and restrict the protocols you want to allow.
const url = new URL(profileUrl, window.location.origin);
if (url.protocol === 'https:' || url.protocol === 'http:') {
link.href = url.href;
}In a real application, you might only allow specific domains or not accept external URLs at all. It depends on the feature.
OWASP stresses this because there is no generic escaping method that works equally for HTML, attributes, URLs, CSS and JavaScript. Browsers parse each context differently. A solution that works for text can be wrong inside an href or a script.
Input validation does not solve everything
When you start learning security, it is tempting to look for a list of forbidden characters. Blocking <script> looks like a quick way out, but it misses many other ways to create content a browser can parse. It can also break legitimate text.
Validation still has a place. If a field is meant to be a postcode, date or fixed category, validate it. XSS prevention also requires deciding how that value is represented when it comes back out.
If you are displaying text, keep it as text. If you are building a URL, validate the URL and use APIs that assign the attribute. If people need to write HTML, escaping everything would break the feature because it would no longer render. That calls for sanitising the HTML with a maintained tool and defining the tags and attributes you accept.
I find that more useful than memorising attacks. Understand what the interface needs to do, then choose the safest way to represent it.
Frameworks help, but they have escape hatches
Modern frameworks usually escape text by default. That removes many mistakes when variables are rendered in a template.
The protection can be bypassed when you use APIs intended to insert HTML. React has dangerouslySetInnerHTML; Vue has v-html; Angular has functions that mark content as trusted; and plain JavaScript has innerHTML, outerHTML, insertAdjacentHTML and document.write.
Those APIs exist because an application may need rich HTML. Using them should be a deliberate choice, with sanitised content and a reason not to use normal text.
In my Astro and MDX projects, article content lives in the repository and goes through the build. That reduces the kind of surface an open comment system or production rich-text editor would have. Any interactive part that reads client-side data still deserves the same question. Does this value end up somewhere that parses HTML?
CSP limits damage, but it does not replace code
In my article about security headers every web developer should know, I covered Content-Security-Policy.
A well-configured CSP can make it harder for injected script to run or load resources. It is a useful layer, especially when the policy matches what the website actually uses.
It does not fix the cause of an XSS issue. A header does not make a careless DOM insertion safe.
The first job is still to keep untrusted values away from APIs that parse them as HTML or code. CSP, HttpOnly cookies where appropriate and other controls then help limit damage if something goes wrong.
What I check around dynamic content
Changing one line of code does not solve XSS across an application. The review has to follow the path of the value and the context where it is used.
When I work on an interface that renders dynamic data, these questions keep me from acting on autopilot.
- Where does this value actually come from?
- Do I need to show text or allow HTML?
- Is there a DOM API that keeps the value as text?
- If it is a URL, which protocols or domains make sense?
- Am I using a framework escape hatch? Why?
- If HTML is allowed, where is it sanitised and who maintains that configuration?
They are simple questions, but they change the review. Instead of thinking “it is only a card” or “it is only a message”, I look at the whole path a value takes before it reaches the browser.
Why I want to keep learning this
Building useful products also means looking beyond the ideal case.
An app can start with a search field, a saved preference or a small form. Then users, external content, sessions, integrations and decisions that are not as harmless as they first looked enter the picture.
XSS matters to me because it connects security with a skill that sits very close to frontend work, putting information on a screen. I am still learning to understand the APIs I use, revisit decisions I would previously have taken for granted and avoid turning data into code for convenience.
That is the kind of learning I want to document while I study cybersecurity and keep building projects.