SharePoint gives you a lot of ways to render user-controlled content, and that’s exactly why XSS keeps showing up in SharePoint customizations. The platform itself has decent guardrails, but the moment you add SPFx components, classic scripts, custom forms, REST-driven UI, or “just a little HTML” from a list field, you can create a mess.

This guide is the practical version: where XSS shows up in SharePoint, what safe code looks like, and what I’d actually recommend in a real tenant.

Where XSS happens in SharePoint

The common sources are boring and predictable:

  • List or library fields rendered as HTML
  • Query string values written into the DOM
  • Search results templates
  • Custom Script Editor / Content Editor web parts on classic pages
  • SPFx web parts using innerHTML
  • Power Apps / JSON formatting / custom forms that inject unsafe strings
  • REST or Graph API data inserted into page markup
  • Rich text fields that are trusted too much

The root problem is almost always the same: untrusted data reaches an HTML, JavaScript, URL, or CSS sink without the right encoding or sanitization.

The big rule: encode for the output context

A lot of SharePoint XSS bugs happen because developers sanitize vaguely instead of encoding specifically.

Use the right defense for the right sink:

  • HTML text node → HTML encode
  • HTML attribute → attribute encode
  • URL parameter → URL encode
  • JavaScript string → JS string encode
  • Rich HTML you intentionally allow → sanitize with a trusted HTML sanitizer

If you remember one thing, make it this: don’t use innerHTML unless you absolutely have to.

Unsafe vs safe DOM rendering in SPFx

This is the classic bug.

Unsafe

public render(): void {
  const userTitle = this.properties.title; // could come from a list, query string, API, etc.

  this.domElement.innerHTML = `
    <div class="banner">
      Welcome ${userTitle}
    </div>
  `;
}

If userTitle contains <img src=x onerror=alert(1)>, you’ve got XSS.

Safe: use textContent

public render(): void {
  const userTitle = this.properties.title;

  const banner = document.createElement("div");
  banner.className = "banner";
  banner.textContent = `Welcome ${userTitle}`;

  this.domElement.replaceChildren(banner);
}

This is the simplest fix and usually the best one.

Safe rendering in React-based SPFx

React helps, as long as you don’t bypass it.

Safe React example

import * as React from "react";

interface IProps {
  message: string;
}

export const WelcomeBanner: React.FC<IProps> = ({ message }) => {
  return <div className="banner">{message}</div>;
};

React escapes content by default. That’s good.

Unsafe React example

export const WelcomeBanner: React.FC<IProps> = ({ message }) => {
  return <div dangerouslySetInnerHTML={{ __html: message }} />;
};

dangerouslySetInnerHTML is where people talk themselves into shipping an incident.

If you truly need to render limited HTML from a rich text source, sanitize first.

Sanitizing rich HTML safely

Sometimes SharePoint solutions really do need to display formatting from a rich text field. In that case, sanitize the HTML and allow only a small set of tags and attributes.

Example with DOMPurify

import DOMPurify from "dompurify";

const dirtyHtml = item.Description; // from SharePoint list rich text field

const cleanHtml = DOMPurify.sanitize(dirtyHtml, {
  ALLOWED_TAGS: ["b", "i", "strong", "em", "p", "ul", "ol", "li", "a", "br"],
  ALLOWED_ATTR: ["href", "title"],
  ALLOW_DATA_ATTR: false
});

container.innerHTML = cleanHtml;

If you do this, keep the allowlist tight. Don’t casually allow style, event attributes, or embedded content.

Query string XSS in SharePoint pages

SharePoint pages often read values from the URL to filter, prefill, or personalize content.

Unsafe

const params = new URLSearchParams(window.location.search);
const category = params.get("category") || "";

document.getElementById("currentCategory")!.innerHTML = category;

Safe

const params = new URLSearchParams(window.location.search);
const category = params.get("category") || "";

document.getElementById("currentCategory")!.textContent = category;

If you need to place the value in a URL, encode it properly.

const url = `/sites/portal/Lists/Products?category=${encodeURIComponent(category)}`;
link.setAttribute("href", url);

Don’t concatenate raw query string values into href, src, or redirect targets.

SharePoint REST API data is not trusted

I still see developers treat data from SharePoint REST as “internal” and therefore safe. That’s a mistake. If a user can edit a field, that field is attacker-controlled.

Unsafe REST rendering

const response = await fetch("/sites/demo/_api/web/lists/getbytitle('Announcements')/items", {
  headers: { Accept: "application/json;odata=nometadata" }
});

const data = await response.json();

const list = document.getElementById("announcements")!;

for (const item of data.value) {
  list.innerHTML += `<li>${item.Title}</li>`;
}

Two problems here:

  • direct HTML injection
  • repeated innerHTML += parsing

Safe REST rendering

const response = await fetch("/sites/demo/_api/web/lists/getbytitle('Announcements')/items", {
  headers: { Accept: "application/json;odata=nometadata" }
});

const data = await response.json();
const list = document.getElementById("announcements")!;

for (const item of data.value) {
  const li = document.createElement("li");
  li.textContent = item.Title;
  list.appendChild(li);
}

That’s the pattern I trust.

Classic SharePoint pages are riskier

Classic pages with Script Editor or Content Editor web parts are where a lot of ugly XSS lives. Old jQuery snippets, inline scripts, and direct DOM manipulation pile up fast.

Unsafe jQuery pattern

var title = GetUrlKeyValue("title");
$("#pageTitle").html(title);

Safer pattern

var title = GetUrlKeyValue("title");
$("#pageTitle").text(title);

If you’re maintaining classic SharePoint, search for these first:

  • .html(
  • .append(
  • .prepend(
  • innerHTML
  • document.write
  • inline event handlers like onclick=
  • string-built templates containing user data

That quick grep usually finds the worst bugs.

A less obvious XSS path is allowing javascript: URLs.

Unsafe

const userUrl = item.Link;
anchor.setAttribute("href", userUrl);

If userUrl is javascript:alert(1), that’s game over.

Safe URL validation

function isSafeHttpUrl(value: string): boolean {
  try {
    const url = new URL(value, window.location.origin);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

const userUrl = item.Link;

if (isSafeHttpUrl(userUrl)) {
  anchor.setAttribute("href", userUrl);
} else {
  anchor.removeAttribute("href");
}

I strongly prefer allowlisting protocols instead of trying to blacklist bad ones.

Content Security Policy helps, but it won’t save sloppy code

CSP is useful in SharePoint-integrated apps and custom pages, especially if you host supporting components yourself. It reduces blast radius when someone misses an encoding bug.

A solid starting point:

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'self' https://*.sharepoint.com;
  img-src 'self' data: https:;
  style-src 'self' 'unsafe-inline';
  connect-src 'self' https://*.sharepoint.com https://graph.microsoft.com;

Real SharePoint deployments often need tuning because of Microsoft 365 endpoints, embedded experiences, and legacy inline styles. If you need a deeper CSP rollout reference, csp-guide.com is useful.

Also, verify your headers from the outside. I usually run a quick check with HeaderTest after changes, because broken CSP and missing framing rules are easier to spot there than by eyeballing config.

SharePoint-specific habits that prevent XSS

These are the habits I’d enforce on any team building on SharePoint:

1. Prefer framework escaping over manual HTML

If React can render it as text, let React do it.

2. Ban raw innerHTML by default

Require a code review exception for it.

3. Treat every list field as untrusted

Even “internal only” lists become attack paths.

4. Sanitize rich text at the boundary

If HTML is allowed, clean it before rendering.

5. Validate URLs before assigning href or src

Allow only http: and https: unless there’s a very specific reason.

6. Reduce classic page scripting

If you still rely on Script Editor web parts everywhere, you’re carrying a lot of risk.

7. Add lint rules

Catch dangerous patterns automatically.

Example ESLint idea:

{
  "rules": {
    "no-unsanitized/property": "error",
    "no-unsanitized/method": "error"
  }
}

That won’t solve everything, but it catches a surprising amount of nonsense.

Quick review checklist

When I review SharePoint code for XSS, I ask:

  • Does any untrusted value reach innerHTML, .html(), or dangerouslySetInnerHTML?
  • Are query string values written to the DOM safely?
  • Are list fields or REST results treated as attacker-controlled?
  • Are rich text fields sanitized before rendering?
  • Are links validated against javascript: or other unsafe schemes?
  • Is CSP present where we control headers?
  • Are classic page customizations still using inline scripts and HTML injection?

If the answer to any of those is bad, I keep digging.

Copy-paste safe utility helpers

These small helpers are worth reusing.

export function setText(el: HTMLElement, value: unknown): void {
  el.textContent = String(value ?? "");
}

export function setSafeLink(el: HTMLAnchorElement, value: string): void {
  try {
    const url = new URL(value, window.location.origin);
    if (url.protocol === "http:" || url.protocol === "https:") {
      el.href = url.toString();
      return;
    }
  } catch {}

  el.removeAttribute("href");
}

import DOMPurify from "dompurify";

export function setSanitizedHtml(el: HTMLElement, dirty: string): void {
  el.innerHTML = DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ["p", "br", "b", "i", "strong", "em", "ul", "ol", "li", "a"],
    ALLOWED_ATTR: ["href", "title"]
  });
}

SharePoint XSS is rarely exotic. It’s usually just untrusted content ending up in the wrong sink because someone wanted to ship quickly. If you avoid HTML injection by default, sanitize the few places that truly need markup, and validate URLs aggressively, you eliminate most of the real-world bugs I see in SharePoint environments.