February 3, 2025
From CSR to DOM, CORS to postMessage — this guide covers the essential web concepts you need to master before diving into XSS and other client-side vulnerabilities.
Ahmed Qaramany
Web development involves creating websites and web applications. It includes the frontend (what users see) and the backend (how it works). The frontend uses HTML, CSS, and JavaScript, while the backend uses languages like Python, PHP, or Node.js. Web developers build, fix, and improve websites.
Client-Side (Frontend)
Server-Side (Backend)
In short, Client-Side is what users see, and Server-Side is where the website's logic and data handling happen.
Every piece of code a developer writes can introduce bugs, depending on their prior knowledge. Code written on the client side (such as HTML, CSS, JavaScript, React, Angular, Bootstrap, etc.) can be exploited by attackers, just like server-side code (such as PHP, MySQL, MongoDB, Node.js, etc.) if not properly secured.
If the code is not properly secured, it can lead to bugs or vulnerabilities.
In the next parts of this blog, we’ll go over the basics you need to understand Client-Side bugs. It’s important to know how things work before jumping into the vulnerabilities.
For example, you won’t fully understand XSS if you don’t know how the payload is sent, where it goes, whether it’s handled by a client-side or server-side function, or if the DOM processes it.
What happens if there’s a Content Security Policy (CSP) or if the HTTPOnly flag is set? These are key things to learn, and we’ll cover everything you need to study Client-Side vulnerabilities properly.
First: CSR (Client-Side Rendering)
Imagine you go to a restaurant, but instead of getting a ready meal, the restaurant gives you all the ingredients and the recipe to cook the food yourself at home. This is like CSR.
In the CSR approach:
You're almost there, but let's refine the flow to make it more accurate in the Client-Side Rendering (CSR) approach:
<div>
(e.g., <div id="root"></div>
) and links to the JavaScript bundles.fetch
, axios
, etc.).This makes it a good fit for applications like chat apps and social media platforms.
Advantages of CSR:
Disadvantages of CSR:
I’m focusing on the client side in this article as much as possible. So, if you’re interested in hunting XSS, you’ll probably find this type of rendering interesting. Why? Because most page updates happen in the browser using JavaScript. This gives more chances to find bugs like DOM-based XSS since JavaScript changes the page content (DOM) a lot.
Here’s an example of the source code from a website that uses CSR.
Belongs to prismic.io
Second: SSR (Server-Side Rendering)
Now imagine going to the same restaurant, but this time, the food is ready, hot, and served on a plate. This is like SSR.
In SSR, the server prepares the whole page with all its content and sends it to the browser. So when you request the page, the server sends a full HTML file with everything inside.
SSR application built with Next.js. The HTML file was rendered on the server, so it contains all the web page’s content, HTML elements, and styles.
Advantages of SSR:
Disadvantages of SSR:
In SSR, most of the work is done on the server before the page goes to the browser. This means there are fewer chances to find DOM-based bugs because JavaScript does less in the browser. However, you can still look for server-side bugs.
Third: SSG (Static Site Generation)
Now imagine the restaurant cooks all the meals at once, packs them in boxes, and stores them. When someone orders food, they just give them a ready box. This is like SSG.
In SSG, the pages are built one time during the build process and saved as ready HTML files. Every request gives back the ready file immediately.
Advantages of SSG:
Disadvantages of SSG:
So, to sum it up:
Caching is an important concept you will likely encounter. A cache is temporary storage that saves data to help websites load faster when you revisit them. The cache provides identical responses to users who make similar requests.
We'll dive deeper into WAFs later in the blog, but for now, let's assume you've heard of Cloudflare and Akamai and think they only function as WAFs. You might notice they block some of your requests when testing websites configured with them, such as XSS, SQLi, XXE, or SSTI attacks. However, it's important to know that they also serve as CDNs.
A CDN (Content Delivery Network) stores copies of your website's content on servers around the world. When someone visits your site, the CDN delivers the content from the server closest to them. This makes the website load faster and reduces the load on the main server.
Many other companies also offer both CDN and WAF services, including Amazon CloudFront, Microsoft Azure, Google Cloud, Fastly, Imperva, and more.
There are different types of caching systems:
Client-side (local): This means the browser saves some data on the user’s computer, so the website can load faster the next time they visit.
Server-side: This means the server saves the responses to some requests for a short or long time. When many users ask for the same thing, the server can give them the saved response instead of processing the request again. This helps the website run faster because the server doesn’t get too busy.
In simple terms, the cache gives the same response to users who make the same request. These saved (cached) responses are kept for a short time.
But what if a bad (malicious or sensitive) response from the site gets saved in the cache, and then this malicious or sensitive response is shown to users who visit the site afterward?
This could lead to different attacks like Information Disclosure, DoS, XSS, and more.
But how does the cache determine if a request is similar?
This is where Cache Keys come into play. When the cache receives a request, it looks at certain details (like the request line, host, and some headers) and compares them to the ones it has already saved.
You can think of these details—called cache keys—as creating a unique fingerprint for each request. If the cache finds a matching fingerprint, it delivers the saved response. If there’s no match, it sends the request to the web server to get a new response.
Here are examples of common cache keys that caching systems use to decide if a request is the same as one they've already saved:
https://example.com/page
).example.com
).GET
, POST
, PUT
, or DELETE
. Usually, only GET
requests are cached.?id=123&sort=asc
).This is a good first step to help you get familiar with Cache Deception and Cache Poisoning bugs.
The DOM (Document Object Model) is a programming interface for web documents. It represents the structure of a web page as a tree of objects, where each object corresponds to a part of the page, such as elements, attributes, or text.
We talked about CSR (Client-Side Rendering) and mentioned DOM-based vulnerabilities. But how exactly is the DOM connected to CSR?
How the DOM is Connected to CSR:
Dynamic DOM Manipulation:
In CSR, JavaScript frameworks like React, Angular, or Vue.js manipulate the DOM dynamically after the page loads. This means the content you see on the page isn’t coming directly from the server-rendered HTML but is being created or updated in real-time through the DOM API.
This is important because it allows scripts (like JavaScript) to dynamically access and update the content, structure, and style of a document.
The DOM Tree:
The DOM represents HTML as a tree structure with nodes like:
Example:
<html>
<body>
<h1>Hello World!</h1>
</body>
</html>
Manipulating the DOM with JavaScript:
getElementById
, querySelector
, getElementsByClassName
, etc.element.innerHTML
, element.textContent
element.setAttribute('class', 'new-class')
appendChild
, removeChild
, createElement
When talking about DOM-based vulnerabilities, two key concepts you need to understand are Sources and Sinks. These help identify how and where user input can lead to security issues.
Sources are the places where user input enters the application. These are points where data comes into the web page, often from the browser or user actions. For example, an HTTP parameter in the URL is considered a source.
Common Sources in the DOM:
location.href
location.search
location.hash
document.URL
document.referrer
window.name
localStorage
, sessionStorage
, cookies
Example:
const userInput = location.hash; // Source: Getting data from the URL hash
Sinks are the places in the code where the user input is inserted or executed. If the input is not properly sanitized, it can cause harmful effects like executing malicious scripts.
Common Sinks in the DOM:
innerHTML
outerHTML
document.write()
eval()
setAttribute()
innerText
/ textContent
(if misused)window.location
(for redirects)Example:
document.getElementById('output').innerHTML = userInput; // Sink: Inserting data into the DOM
When data flows from a source to a sink without proper validation or sanitization, it can lead to DOM-based XSS or other security issues.
If user input isn’t handled properly when updating the DOM, it can cause attacks like adding harmful JavaScript code to the page. This is a simple example of XSS (Cross-Site Scripting).
We won’t go deep into XSS here since we're focusing on the basics you should know before learning about specific bugs. But it’s important to understand that letting attackers run scripts on a page is dangerous, so we need ways to stop this from happening.
One of the best ways to prevent harmful scripts from running is by using Content Security Policy (CSP).
What is CSP?
CSP is like a set of rules for your website that tells the browser which scripts are allowed to run and which are not. Even if an attacker manages to inject a script, CSP can block it from running.
You add a special header to your website that looks something like this:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-source.com;
This means:
'self'
) and trusted sources (https://trusted-source.com
) are allowed. Any other script, even if injected by an attacker, will be blocked.It’s important to note that not having a CSP isn’t considered a vulnerability or a bug. It’s more of a security enhancement that helps strengthen your site’s protection.
Sometimes, when you try to see the page source code using CTRL + U or by typing view-source:https://target.com in the URL bar, you might not be able to see the full content. Let’s explore why this happens.
This is related to the topic we discussed about SSR and CSR.
In CSR:
<div id="app"></div>
.In SSR:
Why Can’t You See Everything in the Page Source?
When you use CTRL + U or view-source:, you're only seeing the original HTML sent by the server. However, many modern websites use JavaScript to change or load content dynamically after the page loads. This means:
Why Inspecting the DOM with F12 Helps:
When you press F12 and inspect the page, you're looking at the live DOM, which reflects all the changes made by scripts running on the page. This is useful because it shows the actual structure and content as the user experiences it, including dynamically loaded elements.
You’ve probably heard of client-side vulnerabilities like CSRF (Cross-Site Request Forgery), CORS (Cross-Origin Resource Sharing), or XSS (Cross-Site Scripting). In this discussion, we'll explore why these issues are referred to as Cross-Site or Cross-Origin and the concepts behind them.
Understanding Origins
Let’s start by looking at how browsers handle requests between different sites. How does this process work? For example, how does Facebook send a request to YouTube? And when you share a YouTube link in a Facebook post, why does a thumbnail appear?
Before anything else, you need to understand the concept of Origin.
In web security, an Origin is defined by three components: the protocol (like http
or https
), the domain (like example.com
), and the port (like :80
or :443
). If any of these components are different, the origins are considered different. For example:
https://example.com
and http://example.com
have different origins because the protocol is different.https://example.com
and https://sub.example.com
have different origins because the domains are different.https://example.com:443
and https://example.com:8080
have different origins because the ports are different.Now that you understand the concept of Origin, it's important to note that the HTTP Origin request header specifies the origin (including the scheme, hostname, and port) that initiated the request.
The Role of the Same-Origin Policy (SOP)
We need Origin headers and the Same-Origin Policy (SOP) to protect users and websites from security threats like unauthorized data access and malicious attacks.
Same-Origin Policy (SOP):
This is a security measure that restricts how documents or scripts loaded from one origin can interact with resources from another origin. This policy helps prevent malicious websites from accessing sensitive data from other sites you might be logged into.
In short, SOP strengthens the security of websites. Now, let’s go back to our example.
When Facebook fetches data from YouTube to display a thumbnail, it's making a cross-origin request.
In this case, Facebook isn't using your browser to make the request. Instead, Facebook's servers send a request directly to YouTube's servers to fetch the necessary information, like the video title, description, and thumbnail. This process bypasses the browser's Same-Origin Policy because it's happening server-to-server.
Here, YouTube allows Facebook to access its thumbnails, and they are okay with displaying them on Facebook.
If YouTube wanted to restrict who can access their resources, they could:
What is CORS (Cross-Origin Resource Sharing)?
On the other hand, if a website tries to make a cross-origin request from your browser, it needs permission through something called CORS (Cross-Origin Resource Sharing). If the server allows it, it will send the correct headers to let the browser know it's safe to share the data. If not, the browser will block the request to protect your data.
You might think that CORS is a vulnerability, but it's not. CORS is a feature that allows websites to share resources with trusted origins. As we mentioned with the Same-Origin Policy (SOP), it strengthens the security of a website by restricting access to resources from different origins.
However, CORS can weaken security if it's misconfigured. Why? Because if a website allows access from untrusted or malicious origins, it can expose sensitive data and make the site vulnerable to attacks.
Example of a Misconfigured CORS:
Let’s take an example: if you have safe-company.com, as part of your security workflow, you should prevent other origins from accessing your site by default, following the Same-Origin Policy (SOP) concept. If you allow requests from other origins without proper restrictions, this could lead to data leakage through unauthorized HTTP requests.
If you use the CORS feature but misconfigure it, you might accidentally trust untrusted origins. That’s why it’s important to carefully manage your CORS configurations to avoid potential security risks.
We're not done with Cross-Site Requests yet; we'll connect this to CSRF and CORS misconfigurations in another section of this blog, inshallah.
AJAX is a way for web pages to send and receive data from a server without reloading the page. It allows you to update parts of a webpage (like showing new content or data) dynamically, making the user experience smoother and faster.
For example, when you click "like" on a post or load new comments without refreshing the whole page, that's AJAX in action.
Fetching Data with AJAX
<!DOCTYPE html>
<html>
<head>
<title>Simple AJAX Example</title>
</head>
<body>
<h1>Click the Button to Load Data</h1>
<button id="loadDataBtn">Load Data</button>
<div id="output"></div> <!-- This is where the data will appear -->
<script>
document.getElementById('loadDataBtn').addEventListener('click', function() {
// Create a new AJAX request
const xhr = new XMLHttpRequest();
// Set up the request: GET method, target URL
xhr.open('GET', 'https://jsonplaceholder.typicode.com/posts/1', true);
// What happens when data is successfully loaded
xhr.onload = function() {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText); // Convert JSON to object
document.getElementById('output').innerHTML = `
<h3>${data.title}</h3>
<p>${data.body}</p>
`;
} else {
document.getElementById('output').innerHTML = 'Error loading data.';
}
};
// Send the request
xhr.send();
});
</script>
</body>
</html>
What Happens Here?
https://jsonplaceholder.typicode.com/posts/1
).<div>
without reloading the page.Expected Output (after clicking the button):
sunt aut facere repellat provident occaecati excepturi optio reprehenderit
quia et suscipit
suscipit recusandae consequuntur expedita et cum
reprehenderit molestiae ut ut quas totam
nostrum rerum est autem sunt rem eveniet architecto
This is the data from https://jsonplaceholder.typicode.com/posts/1
.
You might be wondering why you didn't get a CORS error when fetching data from https://jsonplaceholder.typicode.com/posts/1
. The reason is that the server allows cross-origin requests by setting the correct CORS headers.
When Would You See a CORS Error?
You’d see a CORS error if:
Access-Control-Allow-Origin
header, the browser will block the request.Access-Control-Allow-Origin: https://trusted-site.com
), and your website isn’t on the list, you’ll get a CORS error.PUT
, DELETE
, or custom headers, the browser sends a preflight request (OPTIONS). If the server doesn’t respond correctly, you’ll get a CORS error.Example of a CORS Error:
If you try to fetch data from a server that doesn’t allow cross-origin requests, you’ll see this error in your browser’s console:
Access to fetch at 'https://some-restricted-api.com/data' from origin 'https://your-website.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
As we mentioned before, cross-origin requests can be a security risk if not handled properly. Browsers enforce the same-origin policy, which restricts web pages on one origin (A) from making requests to a different origin (B). However, there are times when cross-origin requests are necessary, such as fetching data from APIs hosted on different domains. This is where CORS (Cross-Origin Resource Sharing) comes into play.
A preflight request is an additional HTTP request that a web browser sends to the server before making a cross-origin AJAX request. The purpose of a preflight request is to check with the server whether the actual request (the one that fetches the resource) is safe to send.
When Does a Preflight Request Happen?
When a web browser makes a cross-origin AJAX request, it determines whether the request qualifies as a simple request or a preflighted request.
GET
, POST
, or HEAD
and are limited to specific content types (like application/x-www-form-urlencoded
, multipart/form-data
, or text/plain
). These requests bypass the preflight process and are sent directly to the server.GET
, POST
, or HEAD
, includes custom headers, or uses non-standard content types, the browser automatically sends an HTTP OPTIONS request to the server before the actual request. This is the preflight request.What’s Included in a Preflight Request?
The preflight request contains extra headers, such as:
https://your-website.com
).PUT
, DELETE
).Authorization
, X-Custom-Header
).How the Server Responds to a Preflight Request
When the server receives the preflight request, it needs to respond with the correct CORS headers to indicate whether the actual request is allowed. These usually include:
GET
, POST
, PUT
, DELETE
).Content-Type
, Authorization
).If the server responds with the correct CORS headers, the browser proceeds to send the actual request. If not, the browser blocks the request and displays a CORS error in the console.
Let’s start by explaining some definitions, beginning with AJAX. When you open Facebook and search for someone named "Mohamed" or search on Google for "how to ...", you see suggestions appear as you type without the page reloading. This is made possible by AJAX, a technology used to update parts of a web page without refreshing the whole page.
We’ve previously seen an example of an AJAX request, but now we need to dive deeper into XMLHttpRequest (XHR), which is at the core of AJAX requests. It’s the tool that allows web pages to communicate with servers without reloading.
What is XMLHttpRequest (XHR)?
XMLHttpRequest (XHR) is a tool in JavaScript that lets you send HTTP or HTTPS requests to a server and receive data back without reloading the page. Even though its name mentions "XML," XHR can be used to fetch any type of data, not just XML. You can use it to get JSON, HTML, or plain text as well.
How Does AJAX Work?
In short, you should now understand that AJAX is a technique used to update parts of a web page without reloading the entire page. It allows web pages to communicate with servers in the background. But how does it do this?
By using XHR, which is a tool or object in JavaScript that makes AJAX possible. XHR is the method used to send requests to the server and receive responses.
A Simple Analogy
Think of AJAX like making a phone call to get information without leaving your house. In this analogy, XHR is the phone you use to make that call.
What About the Fetch API?
Today, many developers prefer using the Fetch API instead of XHR because it’s simpler and more modern. However, it’s still considered AJAX because the concept is the same: fetching data from a server without reloading the page.
function loadUser() {
fetch('user.json') //Get data from `user.json`
.then(res => res.json()) // Convert response to JSON
.then(data => { //Show the data on the webpage
document.getElementById('user').innerHTML = `
<h3>Name: ${data.name}</h3>
<p>Email: ${data.email}</p>
<p>Age: ${data.age}</p>
`;
})
.catch(() => {
document.getElementById('user').innerHTML = 'Error loading data';
});
}
Since we are talking about the browser and how powerful JavaScript is in its role to send requests between origins, and we mentioned SOP a lot in the blog, we need a way of communication that is secure and safe. Basically, it lets different pages or windows send messages to each other even if they’re from different origins.
postMessage() has built-in safety features:
iframe.contentWindow.postMessage('Hello!', 'https://trusted-site.com');
window.addEventListener('message', function(event) {
if (event.origin === 'https://trusted-site.com') {
console.log('Safe message:', event.data);
} else {
console.log('Blocked message from:', event.origin);
}
});
I guess now you understand why it can cause some problems, right?
If you don’t check the message’s origin when receiving data, it can lead to security risks like DOM-based XSS.
Let's understand more with a real example:
1. A Page Talking to a Popup It Opened
Imagine you're on a website, and you click "Login with Google". A popup window opens where you log in. Once you're logged in, the popup needs to tell the original page that you're logged in.
postMessage()
:postMessage()
:2. A Page Talking to an Iframe
Let’s say you embed a YouTube video on your blog using an iframe. The YouTube player might need to send messages to your blog, like telling it when the video starts playing or when it’s paused.
postMessage()
:postMessage()
:There are more examples of this.
While postMessage
is designed for secure communication, if not used properly, it can be exploited in DOM-based XSS attacks. This happens when a web page blindly trusts messages received via postMessage
without validating the origin or content.
Let's suppose we have an index.html
file, which we’ll call the parent page.
<iframe id="myFrame" src="child.html" style="width:300px; height:200px;"></iframe>
<script>
// Listen for messages from the iframe
window.addEventListener('message', function(event) {
// No origin check, blindly trusting any incoming message
document.body.innerHTML = event.data; // Directly injecting message into the DOM (vulnerable)
});
</script>
attacker create malicious Iframe (`child.html`) we call it child page
<button onclick="sendMaliciousMessage()">Send Malicious Script</button>
<script>
function sendMaliciousMessage() {
// Sending malicious script to the parent page
parent.postMessage('<script>alert("XSS Attack!")<\/script>', '*');
}
</script>
This doesn’t cover all the components you might encounter while learning web security, but I’ve shared some key definitions that are essential for a better understanding. We might create another part to dive deeper. Thanks for reading!