Node.js Security Unleashed: Your Ultimate Defense Guide (3/7)

Part 3 -  How to prevent Cross-site Scripting (XSS)

Node.js Security Unleashed: Your Ultimate Defense Guide (3/7)

A Preface

Nowadays, everyone acknowledges that there exists a plethora of possible attacks and exploits, each capable of employing diverse approaches to compromise a targeted system. Thankfully, a significant portion of them derives from the well-established concepts and relies on widely recognized patterns.

Therefore, such threats, though each having its own peculiar characteristics, can still be classified based on their shared traits. By identifying these commonalities, the development community managed to elaborate a comprehensive suite of effective countermeasures against them.

Throughout this article series, we will highlight seven signature attack patterns that we should be concerned about when safeguarding our Node.js applications.

!!! Disclaimer !!!

The information provided in this article is intended for educational purposes only. Readers are encouraged to consult with qualified cybersecurity experts and adhere to their organization’s security procedures when addressing the mentioned attacks. Neither the author nor the hosting platform is responsible for any actions taken by individuals or organizations based on the information contained herein.

Please be prudent, and use the provided information solely with good intentions.


Series Agenda:


How to prevent Cross-site Scripting (XSS) in Node.js

Introduction

Greetings, dear readers!

Welcome to the third installment of our article series dedicated to the theory and practice of defending against prevalent attack patterns in software development.
In this very segment, we will explore the ins and outs of the Cross-site Scripting (XSS) threats, and come to know why we, developers, should be concerned about it.

By the end of this paper, you will be well-equipped with both the general knowledge about XSS attacks and the skills needed to diminish their impact on your applications.

So without further ado, let’s begin our journey…


Understanding XSS

“Cross-Site Scripting (XSS) attacks are a type of injection in which malicious scripts are injected into otherwise benign and trusted websites. XSS attacks occur when an attacker uses a web application to send malicious code, generally in the form of a browser-side script, to a different end user. Flaws that allow these attacks to succeed are quite widespread and occur anywhere a web application uses input from a user within the output it generates without validating or encoding it”.

Excerpted from: https://owasp.org/www-community/attacks/xss/

Simply put, XSS allows attackers to inject a malicious piece of code (e.g. a script or DOM element) into some application's resource with the intent of negatively affecting as many users as possible. This code is then broadcasted to other service clients that have visited this resource, resulting in a burst of data theft, subsequent blackmailing and personalized exploits.

Such a threat derives from browsers` inability to distinguish regular textual blocks from “special” content (like JS scripts or CSS sheets). So, by default, anything enclosed in angle braces is considered a legitimate HTML tag, whereas everything within the <script> tag is acknowledged as JavaScript code.

If certain text needs to only appear as "special", but not be treated as such, the server side needs to validate and filter it before persisting to its data storage, while the client side has to encode it before displaying the same content to the user.
And, if either of them fails their respective duty towards a malicious user input, the embedded code will be evaluated as part of a web page's rendering process, causing unpredictable consequences for the targeted software system.


Real-World Scenario

Imagine the following situation: an emerging e-commerce platform allows its users to describe their impressions of the recently purchased products.
The comment text area is purposefully designed to provide a rich-text editing (RTF) functionality, including bold text, italics and even simple code segments.

When a user navigates to the product page, these comments are fetched from a remote data source and displayed without proper sanitization and markup-to-text encoding.

Exploiting such oversight, an unknown user injects a harmful JavaScript script into the comment on a popular offering's page. The code within places an order for 10 units of an expensive product on behalf of the marketplace visitor. Due to the mentioned security vulnerability, the malicious script is treated as legitimate textual content and not as a threat, eventually causing substantial financial and reputational damage to the seller company.

Here is a superficial example of the vulnerable application in question:

1. Server-rendered Express.js API:

/* application.ts */
import Express from 'express';
import { urlencoded } from 'body-parser';

const app = Express();

// Enabling the view engine to handle `EJS` templating language
app.set('view engine', 'ejs');
app.set('views', './pages');

// Parsing `application/x-www-form-urlencoded` form bodies
app.use(urlencoded({ limit: '50kb', extended: true }));

declare type ProductComment = {
  refId: number | string;
  content: string;
};

// A naïve in-memory data storage for product comments
// Any database integrations are intentionally omitted for brevity
const commentsDb = new Map<string, Array<ProductComment>>;

// Route that handles insertion of new comments
app.post('/products/:productId/comment', (req, res) => {
  const { productId } = req.params;
  const { inputComment } = req.body;

  if (!productId || !inputComment) {
    return res.sendStatus(400);
  }

  const newComment: ProductComment = {
    refId: productId,
    // Assume a harmful JS script is sent within the request body
    comment: inputComment,
  };

  const productComments = commentsDb.get(productId) ?? [];
  productComments.push(newComment);

  // Appending new comment to our superficial database
  commentsDb.set(productId, productComments);
  res.status(201).json(newComment);
});

// Route to display comments with SSR
app.get('/products/:productId', (req, res) => {
  const { productId } = req.params;

  // Get comments for the specific product
  const prodComments = commentsDb.filter(({ refId }) => refId === productId);
  // Render the product page template and pass the comments to it
  res.render('product', { productId, comments: prodComments });
});

app.listen(8080, '127.0.0.1');

2. A complementary EJS template (pages/product.ejs):

<!-- `pages/product.ejs` EJS template -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Product Page</title>

    <!-- Include Quill stylesheet & init script for RTF support -->
    <link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
    <script src="https://cdn.quilljs.com/1.3.6/quill.js"></script>
  </head>
  <body>
    <h1>Product Page - <%= productId %></h1>
    <h2>Comments:</h2>
    <ul>
      <% comments.forEach((comment) => { %>
        <li>
          <!-- `<%- ... %>` placeholder instructs EJS to
          NOT escape enclosed content -->
          <p><%- comment.content %></p>
        </li>
      <% }); %>
    </ul>

    <!-- Add a form for posting comments with Quill editor -->
    <form action="/products/<%= productId %>/comment" method="post">
      <label for="comment">Add a comment:</label>
      <div id="editor" style="height: 150px;"></div>
      <input type="hidden" id="comment" name="comment">
      <button type="submit">Submit</button>
    </form>

    <script async>
      // Initialize Quill editor
      const quill = new Quill('#editor', { theme: 'snow' });
      const formElement = document.querySelector('form');

      // Update the hidden input with the input HTML when submitting the form
      formElement.addEventListener('submit', () => {
        const commentElement = document.querySelector('#comment');
        commentElement.value = quill.root.innerHTML;
      });
    </script>
  </body>
</html>

XSS Classification

In general, the most common Cross-site Scripting threats may be grouped into three main categories. Each type has its own identifiable characteristics and causes peculiar impacts on vulnerable applications:

  • Persistent (stored) XSS.

    A malicious code is injected into the application’s storage (e.g. a disk, a database), and gets spread to other users on behalf of the trusted application. An example provided in the section above is just an instance of this specific attack type.

  • Reflected (non-persistent) XSS.

    An attacker crafts a special link (based upon some URL present in your application) that executes a malicious code once it has been followed in a browser. This is a “softer” type of XSS attack, as it can only affect a single user - the one who has opened a link.
    For example, the numeric pagination search parameter may be replaced with a malicious code string. The page itself may fail to load (or not necessarily), but the script will run nonetheless.

  • DOM-based XSS.

    An attacker utilizes the application's reliance upon insecure DOM interactions (e.g. a combination of empty src attribute and onerror attribute on the <img> tag) and/or properties (like element.innerHtml or document.location) to execute a malicious code.
    While this primarily is a client-side vulnerability, it can just as well affect your Node.js application if it is used as a Server-Side Rendering (SSR) engine.


XSS Threat in Single-page Applications

The rapid rise of Single-Page Applications (abbr. SPAs) has initiated a paradigm shift in web development, leading to the facilitation of a seamless and responsive user experience by loading the entire application within the user's browser. However, such architecture has also introduced a set of unique challenges connected to XSS-related threats.

Due to SPAs` inherently dynamic nature, the displayed content is often loaded and updated asynchronously using client-side JavaScript, making them rather prone to malicious code injections.

The majority of conventional server-reliant measures do not apply to such applications, hence requiring a more nuanced approach to managing security.

Now, let us discuss the prevalence of the stated XSS attack categories in a typical SPA environment:

1. DOM Based XSS.

  • Prevalence: Highest

  • Explanation: SPAs heavily utilize dynamic client-side rendering, relying on direct DOM interactions to update the displayed user content. Such reliance creates a major opening for any HTML document manipulations, including XSS attacks.

2. Persistent XSS.

  • Prevalence: Moderate to High

  • Explanation: SPAs typically update content without the full-page reloads, often fetching data from APIs and seamlessly applying the changes in the background. Such a dynamic mechanism amplifies the impact of Persistent XSS attacks.
    If user inputs are not properly validated and sanitized before storage, an injected script can persist and affect users across multiple sessions.

3. Reflected XSS.

  • Prevalence: Moderate

  • Explanation: The Reflected XSS in the SPA context is often linked to manipulating URLs and making malicious API requests. While still significant, such attacks are typically less severe as their impact is constrained to individual users.


XSS Threat in Server-Rendered Environments

The classical server-rendered environments, where pages are generated on a server and sent as complete HTML documents to a client, historically faced XSS threats despite providing an initial layer of security. While such an approach greatly reduces client-side JavaScript execution and disallows code tampering out of the box, XSS vulnerabilities may still arise.

The attackers can attempt to inject malicious scripts into the generated content, form fields or other input areas. Pages that dynamically include user input without proper filtering and encoding are the main targets of XSS, compromising the security of the entire application if an attack succeeds.

Having established that, let us explore how the designated XSS attack categories manifest in a traditional SSR setting:

1. Persistent XSS.

  • Prevalence: Moderate to High

  • Explanation: In SSR environments, all user content is generated (and stored) on the server side, which increases the motivation for Persistent XSS attacks.
    With the server in play, developers inherently gain more control/authority over user input handling, as well as its validation & sanitization policies.

    To some extent, this mitigates the Persistent XSS, as the user-generated content undergoes the server-side regulations before being rendered.
    To a certain degree, this helps alleviate Persistent XSS concerns, as user-generated content is subject to server-side regulations before being presented.

2. Reflected XSS.

  • Prevalence: Moderate

  • Explanation: Similar to SPAs, the success of such attacks relies on crafting convincing malicious URLs and enticing users to access them.
    With Reflected XSS, it's not important what environment our application is rendered in, only the aspect of social engineering matters. Thus, attack vectors remain the same.

3. DOM Based XSS.

  • Prevalence: Lowest

  • Explanation: Server-rendered environments typically involve much less client-side processing than SPAs. Since the server generates the HTML markup before sending it to clients, the opportunities for DOM-Based XSS are greatly reduced.


Defense Strategies

Now that we classified all known XSS threats and become acquainted with their prevalence in both client- and server-side environments, let us explore how we can prevent such attacks from ever reaching our software systems.

1. Safeguard Untrusted Input

As always, the first technique that comes to mind whenever discussing general software security is the implementation of proper user input handling logic.
To prevent malicious payloads from causing stated XSS threats in your application, we would recommend you follow these three essential steps:

Validate against expectations.

Verify any received user’s input against a predefined data schema. Reject its processing if any incompliances are found.

Filter (sanitize) on receipt.

Define a set of terms (whitelist/blacklist) or specific data patterns that should be kept/stripped off from the received request input.

Encode (escape) before rendering.

Convert any contextually dangerous characters to their safe equivalents before displaying "special" data to the user.
For instance, if some textual block needs to safely display an HTML markup, any angle brackets (> and <) should be replaced with their Entity equivalents (&gt; and &lt;).

— — — — — — — — — — —

As for the Node.js ecosystem, a popular validator.js library is typically used to validate, sanitize and escape arbitrary text-based data altogether. It provides an extensive collection of validators & sanitizers that can be used to create granular validation policies.
This package also offers an Express.js middleware integration that we will gladly take advantage of:

import Express from 'express';
import { json } from 'body-parser';
import { body, matchedData, validationResult } from 'express-validator';

const app = Express();
app.use(json({ limit: '50kb' }));

// Defining the route Validation Policy
const escapedMessage = body('message').notEmpty().escape();
const validatedEmail = body('email').trim().isEmail();

// Applying the validation middlewares
app.post('/', escapedMessage, validatedEmail, (req, res) => {
  const { message, email } = req.body;
  const result = validationResult(req);

  console.log(`Initial data: ${message}---${email}`);

  // If no errors were spotted
  if (result.isEmpty()) {
    // Retrieving the successfully escaped/filtered input data
    const { message, email } = matchedData(req);
    return res.send(`Hello, ${message} from ${email}!`);
  }

  res.status(400).send({
    message: 'Failed validation',
    // Sending the validatino errors bad to the client
    errors: result.array(),
  });
});

app.listen(8080, '127.0.0.1');

— — — — — — — — — — —

If you require a more "low-level" and configurable alternative, consider exploring the options provided by the xss library. This package was specifically created to help mitigate XSS-related security concerns and safeguard textual data. It works for both client-side (browser SPAs) and server-side (Node.js SSR) environments.

For example, to filter out all HTML tags/entities from the markup and keep only the enclosed text you may refer to the following code sample:

import { filterXSS, escapeAttrValue } from 'xss';

const input = `
  <div>
    <strong>Hello,</strong>
    <script async>window.alert('XSS');</script>
    world!
  </div>
`;

const html = filterXSS(input, {
  whiteList: {},
  // filter out any HTML tags
  stripIgnoreTag: true,
  // the script tag is a special case, filter it out also
  stripIgnoreTagBody: ['script'],
});

// Will log `**Hello,world!**`
console.log(`text: ${html.replace(/\\s/g, '')}`);

2. Content Security Policy (CSP)

In addition to the recommended best practices discussed above, there also exists another well-established mechanism created specifically for mitigating XSS threats. It is called Content Security Policy (abbr. CSP) and is most commonly represented by the HTTP response header.

TL;DR: The main takeaway is that CSP helps you define rules for the loading and execution of external resources on your website, and thus provides an extra layer of security against XSS attacks of any type.

— — — — —— — — — ——

What exactly does CSP refer to?

CSP is a modern security mechanism that greatly assists in the prevention of XSS attacks by regulating the resources that can be loaded by a web page. It does so by adhering to a custom ruleset dictating how a web page may interact with external sources.
You may think of it as a list of guidelines that a website strictly follows to ensure that only trusted resources can be loaded and executed.

This regulation allows us to determine what domains are authorized to load certain content on a web page, effectively preventing any unauthorized sources from script execution. So, even if some attacker tries to inject malicious resources, a properly configured CSP significantly reduces the risk of their harmful effect on a website.

The CSP primarily focuses on safeguarding web servers, such as Nginx or Caddy, and APIs that deliver static assets. Our clients (their browsers) already possess this feature in place, and our role as server administrators is solely to ensure its enforcement.

For example, consider the following header value:
Content-Security-Policy: default-src 'self'; script-src 'self' trusted-scripts.com;
Such configuration explicitly restricts script loading to only the same origin as the website and the 'trusted-scripts.com' domain, blocking all other script sources.
Any other resources (not scripts) can only be loaded by the same origin ("self").

— — — — — — — — — — —

Configuring Content Security Policy.

Content Security Policy has evolved through three major versions, each introducing various features and improvements. So, let’s get acquainted with the main directives provided throughout CSP versions 1-3:

  1. default-src — Sets the default origin(s) (usually the website itself) for loading various types of resources (like scripts, styles, images, etc.).
    If other directives haven't specified a source list, this one is used as a fallback.

  2. script-src — Controls which sources are allowed to execute JavaScript on the page, including inline scripts, external scripts, and data URIs. Amongst others, this directive has two special options:

    • ‘unsafe-eval’ — controls whether inline JS code (incl. eval, new Function, setTimeout and setInterval) is allowed to be evaluated.

    • ’unsafe-inline’ — controls whether inline script and style tags (incl. event handler attributes like onclick) are allowed to be executed.

  3. style-src — Controls the sources from which stylesheets may be loaded or applied, including inline styles, external stylesheets, and data URIs.

  4. img-src — Specifies the sources from which images may be loaded.

  5. font-src — Specifies the sources from which web fonts may be loaded.

  6. media-src — specifies the sources for media elements (audio and video).

  7. connect-src — specifies the origin(s) that a website is allowed to make network requests to.

  8. frame-src — defines the sources from which iframes may be loaded.
    Represents a subset of frame-ancestors directive.

  9. object-src — specifies the sources from which the embedded content (embed, object) may be loaded and executed.
    Represents a subset of frame-ancestors directive.

  10. frame-ancestors — specifies external URLs that may embed a current website using <frame>, <iframe>, <object>, or <embed>. This directive essentially represents the next generation of X-Frame-Options header.

  11. report-uri — specifies a URL where reports are sent if the CSP rules are disobeyed. By default, any CSP-related error is logged into the user’s console.
    If we specify a custom URL, we can monitor and take action regarding CSP violations.

  12. worker-src — defines the sources from which Web Workers or Service Workers may be loaded and executed.

  13. base-uri — specifies URLs which may be used in a document's <base> element.

— — — — — — — — — — —

Using Nonces with CSP.

Besides some of the discussed directives, CSP v2 has introduced another feature that enables more granular control over the inline HTML content called “nonce” (abbr. for “number used once”). Conceptually, the nonce is a random unique identifier used to mark certain inline scripts and styles to associate them with corresponding CSP policies (script-src and style-src). Programmatically, nonce represents a cryptographic token of arbitrary entropy, generated for each distinct HTTP response.

By utilizing nonces, you can specify exactly what inline scripts & styles are allowed to be run, ensuring that only authorized and trusted code is executed.

To start using them, include the 'nonce-' option within your CSP policy configuration.
For example:

  • For scripts: script-src 'nonce-<your-nonce-value>' <...>

  • For styles: style-src 'nonce-<your-nonce-value>' <...>

— — — — — — — — — — —

Implementing CSP in Practice.

To make our Node.js web server benefit from the CSP features, we will take advantage of the excellent library called helmet.
This library, amongst others, provides a contentSecurityPolicy middleware that may be applied for setting the CSP header in every outgoing server response:

/* csp.ts */
import { contentSecurityPolicy as csp } from 'helmet';
import type { Request, Response, NextFunction } from 'express';
import { randomBytes } from 'node:crypto';

type FirstParam<T> = T extends (first: infer TArg, ...args: unknown[]) => unknown ? TArg : never;
type CspDirectives = (FirstParam<typeof csp> & {})['directives'];

// middleware that generates a random "nonce" value that gets applied within CSP
export function populateCspNonce(_: Request, res: Response, next: NextFunction) {
  res.locals.cspNonce = randomBytes(16).toString('hex');
  next();
}

function getNonce(_: Request, res: Response): string {
  const nonce = res.locals.cspNonce;
  return `'nonce-${nonce}'`;
}

const nonceBasedAssetPolicy: Partial<CspDirectives> = {
  // allows only local (nonced) scripts as well as Google Analytics
  // browsers that support 'strict-dynamic' will ignore every other option
  // allowing only local scripts to propagate further loading
  scriptSrc: [
    getNonce,
    "'strict-dynamic'",
    // browsers that don't support 'strict-dynamic' will instead
    // allow 'self' as well as anything within domain whitelist (google-analytics)
    // such configuration provides great backwards-compatibility
    "'self'",
    '*.google-analytics.com',
    "'unsafe-inline'",
  ],
  // allows only local stylesheets and CDN domain that loads TailwindCSS styles
  styleSrc: [getNonce, '*.cdn.tailwindcss.com'],
};

const miscDirectives: Partial<CspDirectives> = {
  baseUri: ["'none'"],
  // by default, all errors will be logged into users' consoles
  // we can override this behavior and instead send CSP violation reports to a custom URL
  reportUri: '<https://your-domain.com/api/csp-violation>',
  // any top-level redirections to the current origin will always be eleveted to **HTTPS**
  upgradeInsecureRequests: [],
};

const resourcePolicy: Partial<CspDirectives> = {
  // by default, the content sources are restricted to the same origin
  // in our case, this would correspond to workerSrc, mediaSrc, baseUri
  defaultSrc: ["'self'"],
  // disallows any embedding content (<embed>, <object>)
  objectSrc: ["'none'"],
  // allows iframes from the same origin and Google Maps API
  frameSrc: ["'self'", '<https://www.google.com/maps/embed/>'],
  // allows images from any secure (HTTPS) sources and data URIs
  imgSrc: ['https:', 'data:'],
  // allows fonts from the same origin and Google Fonts API
  fontSrc: ["'self'", '*.fonts.googleapis.com'],
};

export const cspPolicy = csp({
  useDefaults: false,
  directives: {
    ...resourcePolicy,
    ...nonceBasedAssetPolicy,
    ...miscDirectives,

    // allows network request to any subdomain of "trusted-domain.com" using HTTP(S) & WS(S)
    // as well as  any subdomain of "atlassolutions.com"
    connectSrc: [
      'https://*.trusted-domain.com',
      'ws://*.trusted-domain.com:*',
      'wss://*.trusted-domain.com:*',
      '*.atlassolutions.com',
    ],
  },
});
/* application.ts */
import Express from 'express';
import { join } from 'node:path';
import { populateCspNonce, cspPolicy } from './csp';

const app = Express();

app.use(populateCspNonce);
app.use(cspPolicy);

// CSP middleware must be initialized first,
// otherwise it won't influence the serverd static content
app.use(Express.static(join(__dirname, '../', 'public')));

app.get('/', (req, res) => {
  res.end('Hello World!');
});

app.post('/api/csp-violation', (req, res) => {
  console.log(req.body);
  res.send('Metrics were received.');
});

app.listen(8080, '127.0.0.1');

— — — — — — — — — — —

As a result, helmet has produced the following CSP policy for us:

default-src 'self';
script-src 'nonce-c92dea0253eb837e6b2f3ea845bdcfba' 'strict-dynamic' 'self' *.google-analytics.com 'unsafe-inline';
style-src 'nonce-c92dea0253eb837e6b2f3ea845bdcfba' *.cdn.tailwindcss.com;
img-src https: data:;
font-src 'self' *.fonts.googleapis.com;
frame-src 'self' <https://www.google.com/maps/embed/>;
object-src 'none';
connect-src https://*.trusted-domain.com ws://*.trusted-domain.com:* wss://*.trusted-domain.com:* *.atlassolutions.com;
base-uri 'none';
report-uri <https://your-domain.com/csp-violation>;
upgrade-insecure-requests;

You can verify the legitimacy of this or any other CSP policy against the established security standards using the great CSP Evaluator toolkit.


3. CT Sniffing and "X-Content-Type-Options" Header

To establish a solid defense against all kinds of XSS threats, we should consider a broader scope of possible attack vectors - not just the classical XSS classification.

One of them is "Mime Type Sniffing", which targets an innate browser mechanism to presume the file type if a server itself fails to provide adequate information.

💡
MIME Type (Content-Type) Sniffing. A technique used by most web browsers to infer the actual MIME type of a file based on its content, rather than rely on the value defined in the Content-Type header. Browsers usually won't opt in for sniffing unless a server explicitly specifies this header in a response, or the specified type cannot be recognized. While this enables browsers to present any file asset in a correct format, it also introduces a significant security risk. Attackers can trick a website into interpreting a file as a different MIME type from its original, which allows them to tamper with the loading & execution process.

Illustrative flow of the browser's Content-Type decision process.

— — — — — — — — — — —

Even though XSS and CT Sniffing are not directly related, they can still intersect under certain conditions. For example:

  • An attacker might attempt to use CT Sniffing to trick the browser into interpreting a file as JavaScript code, so forth creating XSS vulnerabilities.
    So, by default, if a browser doesn't receive a Content-Type header and decides that file contents look like legitimate JavaScript code, it would be implicitly treated as application/javascript.
    Such behavior can result in a "Drive-by Download" threat, extensively used for phishing attacks.

  • In some cases, an attacker might rely on MIME confusion (exploiting differences in CT interpretation between the server and the browser) to deliver a file that is treated as a script, thus opening doors for the XSS attack.

— — — — — — — — — — —

The X-Content-Type-Options is, again, a security header designed to directly protect applications from Content-Type sniffing; and indirectly, from XSS attacks.

It accepts a single possible value of nosniff, which instructs the browser to always rely on the MIME type declared in the Content-Type header and completely opt out of type sniffing. For instance, if the server responds with a plaintext file but marks its CT as text/html, the browser would render an HTML instead of implicitly changing its type to text/plain.

Besides the obvious defensive value, the X-Content-Type-Options header can help ensure that a website is displayed consistently across all modern browsers (as each can handle certain MIME types differently).


Cookies, commonly employed as a primary client-side storage mechanism, have always been drawing the attention of malicious actors. Hence, a significant portion of XSS attacks involves injection of the client-side JavaScript code aimed at reading or manipulating the document.cookie property, often altering its value.

To address such security risks, we can confine the cookie access strictly to the server, keeping the client side unaware that such cookies even exist.

By marking a cookie with the HttpOnly flag, we instruct the client's browser to prevent client-side scripts from accessing its contents.
Such "flagged" cookies are only included in server HTTP requests (through the Cookie header) and are otherwise inaccessible to JavaScript running in the browser.

So, even if an attacker manages to inject malicious scripts into a web page, they won't be able to access or modify the information stored in HttpOnly cookies.

This restriction gives an additional layer of protection to authentication tokens and session identifiers, making it significantly more challenging for attackers to compromise sensitive data.

— — — — — — — — — — —

In Express.js, you can leverage the res.cookie(...) method to apply additional configuration to a response cookie:

import Express from 'express';

const app = Express();

app.get('/some-route', (_, res) => {
  res.cookie('<cookie-name>', '<cookie-value>', {
    // Applying the `HttpOnly` flag
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    // other configuration...
  });

  res.sendStatus(200);
});

app.listen(8080, '127.0.0.1');

— — — — — — — — — — —

This can also be done in the vanilla Node.js server:

 import { createServer } from 'node:http';

 const server = createServer((_, res) => {
   // Applying the `HttpOnly` flag
   const cookieValue = '<cookie-name>=<cookie-value>; <...>; HttpOnly';
   res.setHeader('Set-Cookie', cookieValue);

   res.end('OK');
 });

server.listen(8080, '127.0.0.1');

5. "X-XSS-Protection" Header Relevancy

Technically, there exists another layer of defense against XSS attacks. It is the security header called X-XSS-Protection, which represents a legacy mechanism that is only available within Internet Explorer 8 (IE 8) and higher. However, it is considered to be more “invasive” than defensive because of its buggy nature that allows attackers to bypass other XSS countermeasures, and pass the initial request filtering.

Therefore, as of late 2023, its usage is highly discouraged, and we recommend to completely disable it (set to “0”) — X-XSS-Protection: 0. This header is automatically disabled in the helmet middleware, so you don’t need to configure anything extra.


Conclusion

In this part of our series, we have taken a deep dive into the world of XSS threats, which has provided us with a solid understanding of this attack pattern, as well as essential knowledge of mitigation mechanisms that constrain such threats.
With all this, you are now well-equipped to secure any server-side applications and get ready to continue exploring the intricacies of CSRF on your own.

Stay vigilant, validate inputs rigorously, and embrace a holistic approach to server-side security.

Additional Resources

For further exploration of the XSS vulnerabilities and security best practices, you can refer to these external resources:

In the next article…

Very soon, we will continue to enhance our comprehension of the prevalent attack patterns within the domain of web security. In particular, we shall delve into the fundamental principles of an entirely different, yet still dangerous attack pattern - a Denial of Service (DoS/DDoS).

If that sounds interesting, stay tuned for further updates!
Until then, stay secure and happy coding!