TypeScript has come a long way to become a part of the modern web development tech stack, be it front end or back end. Developers who transition to TypeScript almost never look back! However, even with a robust programming language under your belt, you could run into the most basic security caveats.
TypeScript strongly compliments modern front-end frameworks like React and Angular. However, it doesn't automatically safeguard your code against DOM vulnerabilities. In fact, many times, developers don't use TypeScript to its full potential, making their codebase vulnerable to DOM XSS attacks. Other times, they just don't know what XSS is and fall prey to vicious attackers injecting their DOM with malicious JavaScripts.
So in this post, I'll talk about what XSS is and how an XSS attack takes place. I'll then show you how you can prevent XSS attacks in your TypeScript projects.
What Is XSS?
XSS stands for "cross-site scripting." It's a technique of injecting external JavaScript into a website. Most of our website's logic, including animations and interactivity of a user, is owned by the JavaScript of our website. Even if you use TypeScript at runtime for building your React or Angular project, at the end of the day, all that TypeScript would be compiled down to JavaScript.
When your application leaves a void that allows an attacker to run some JavaScript that you didn't authorize, your website is under an XSS attack. That void itself is an XSS vulnerability of your application. But what does an XSS vulnerability look like? And how can an XSS attack really happen?
How Does an XSS Attack Happen?
Let's take a simple example of a website where you're filling out a form. For brevity, I have a simple CodeSandbox example that renders an input field and a Submit button on the page. This is how the page appears:
What the page looks like.
And here's what its HTML looks like:
<!DOCTYPE html>
<html>
<head>
<title>XSS Attack Example</title>
<meta charset="UTF-8" />
</head>
<body>
<div id="app">
<h1>Enter your name</h1>
<input type="text" />
<button>Greet Me!</button>
<div id="greeting"></div>
</div>
<script src="src/index.js"></script>
</body>
</html>
When a user writes in their name and presses the Submit button, we run a simple JavaScript function. This function captures the value in the input field. It then generates a greeting for the user right underneath it. So if you were to type out your name and hit Submit, this is what you'd get:
An example of the greeting you'll see.
Now, this might seem safe and sound, right? Let's take a look at the JavaScript code that does all this:
import "./styles.css";
const input = document.querySelector("input");
const submitButton = document.querySelector("button");
const greetingBox = document.querySelector("#greeting");
function onClick() {
const name = input.value;
greetingBox.append(`Hey ${name}! A very good morning to you`);
}
submitButton.addEventListener("click", onClick);
If you look closely at where we're manipulating the DOM, we're appending some new HTML using the append method on a DOM element. Does this sound safe to you?
Well, it isn't! It's a breeding ground for XSS vulnerabilities. All this JavaScript code is available to anyone via the browser, so I could just go ahead and modify that line to be like this:
greetingBox.append(alert("you are hacked! 🐱💻"));
And now if anyone presses the Submit button, the alert function is executed:
The alert you'd see.
That's what an XSS attack is in a nutshell. If you're curious and wish to know more about it, check out this in-depth post that dives deeper into it.
XSS Example in TypeScript
The previous example was written in JavaScript, but its TypeScript counterpart wouldn't be much different either. To demonstrate, let's take the example of a React with TypeScript project where we can replicate the above scenario.
Here's the example for your reference. All we have is a simple React app with the following code in our App.ts file:
import "./styles.css";
export default function App() {
const handleSubmit = () => {
const inputDOM: HTMLElement | null = document.querySelector<
HTMLInputElement
>("input");
const inputVal: string = inputDOM?.value;
const greetingBox = document.getElementById<HTMLElement>("greeting");
greetingBox?.append(inputVal);
};
return (
<div className="App">
<h2>XSS in Typescript + React</h2>
<input />
<button onClick={handleSubmit}>Submit</button>
<div id="greeting"></div>
</div>
);
}
If you look at all our functions and DOM manipulations, we're using full-on TypeScript instead of plain old JavaScript! However, the way we're manipulating the DOM is still incorrect. It still exposes a DOM XSS vulnerability. That means just using TypeScript won't help.
In the above codebase, let's say that an attacker manages to inject a script and programmatically executes this:
...
greetingBox?.append(alert("you are hacked! 🐱💻"));
...
The XSS attack could very well be executed:
The alert you'd see.
But the solution to the above problem is simple: there's some bad code that needs refactoring. Let's look at it now.
Prevent XSS in Your TypeScript Project
In the vanilla JavaScript example, here's the change you need to make. Instead of using the append method to append some text to the DOM, use textContent as shown:
greetingBox.textContent = `Hey ${name}! A very good morning to you`;
The above would yield the same result. And if an attacker tries to add some JavaScript to it:
greetingBox.textContent = `alert('you are hacked!')`;
The JavaScript will be rendered as a string on the HTML page instead of being executed:
The text the attacker will see.
Now let's go back to our React and TypeScript project. There's a ton of bad code. We're directly manipulating DOM without using state and ref, which could really come in handy here.
import "./styles.css";
import { useState } from "react";
export default function App() {
const [name, setName] = useState<String>("");
const handleSubmit = () => {
const inputDOM: HTMLElement | null = document.querySelector<
HTMLInputElement
>("input");
const inputVal: string = inputDOM?.value;
const greetingBox = document.getElementById<HTMLElement>("greeting");
setName(inputVal);
// greetingBox?.append(inputVal);
// greetingBox?.append(alert("you are hacked! 🐱💻"));
};
return (
<div className="App">
<h2>XSS in Typescript + React</h2>
<input />
<button onClick={handleSubmit}>Submit</button>
<div id="greeting">{name}</div>
</div>
);
}
Instead of using the append method, we simply set the value of the input field to be that of the state. We then use this state to output the value on the DOM.
If you want to learn more about how to prevent XSS attacks in React, head over to our guide on it.
Unsanitized HTML Can Cause XSS Attacks
We've seen a way to work around DOM-based XSS vulnerabilities. But what about cases where you absolutely need to set HTML inside a container? For instance, you could have a rich text editor that would save the formatted text in the database. But when this text comes back, it comes along with its relevant HTML tags.
Or in other cases, your server could return hrefs of some dynamic link your TypeScript application needs to render. In that case as well, you'll need to programmatically manipulate DOM.
For brevity, let's consider a scenario where your back end returns the href of a link that your Angular + TypeScript application renders. Here's the HTML part of the component:
<div>
<h3>
Welcome to XSS with Typescript in Angular!
</h3>
<a class="link">Click</a>
</div>
In its component.ts file, we can set the href property of the <a> tag. Here's how:
import { Component, Renderer2, ElementRef } from "@angular/core";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
constructor(private renderer: Renderer2, private el: ElementRef) {}
url: string = "https://google.com";
private get host(): HTMLElement {
return this.el.nativeElement;
}
ngOnInit() {
const link = this.getLink(".link");
this.renderer.setAttribute(link, "href", this.url);
}
private getLink(selector: string): HTMLAnchorElement {
return this.host.querySelector(selector);
}
}
If you click on the above link, you should be redirected to google.com. But in this case, imagine the href coming from an API request. In that case, an attacker could easily manipulate the href of the URL to be this:
url: string = 'javascript:alert("you are hacked 🐱💻")';
And now we're back to square one! Our Angular + TypeScript application has an XSS vulnerability. So what do we do now?
Sanitize HTML to Prevent XSS Attacks
Well, we should sanitize our HTML! Angular provides us with a library called DomSanitizer as a part of its platform-browser module. We can import it in our component.ts file like this:
import { DomSanitizer } from "@angular/platform-browser";
Create a reference of it inside our constructor:
constructor(
...
private DomSanitizer: DomSanitizer,
...
)
{}
We can now use DomSanitizer to sanitize any HTML according to a specific rule. Since we want to sanitize an HTML URL, we can use the SecurityContext module from the Angular core module.
import { SecurityContext } from '@angular/core';
Finally, we can now sanitize our URL like this:
url: string =this.DomSanitizer.sanitize(SecurityContext.URL, 'javascript:alert("you are hacked 🐱💻")';
And now if you try to click the link, the alert should not appear. If you head over to the console, you'll see something like this:
The warning you'll see after sanitizing.
That's how you can sanitize your unsanitized HTML in your Angular + TypeScript applications to safeguard such use cases against lethal XSS attacks. You can read more about how to prevent XSS attacks in Angular applications in our guide on that topic.
Conclusion
XSS vulnerabilities are most commonly introduced in DOM manipulations. If you are careful about how you're manipulating your DOM, avoiding them would be a breeze. You must also be careful when directly outputting HTML on the page. Developers often forget to sanitize this HTML, leading to XSS vulnerabilities that attackers may use to launch lethal attacks on your application.
This post was written by Siddhant Varma. Siddhant is a full stack JavaScript developer with expertise in frontend engineering. He’s worked with scaling multiple startups in India and has experience building products in the Ed-Tech and healthcare industries. Siddhant has a passion for teaching and a knack for writing. He's also taught programming to many graduates, helping them become better future developers.