Workspace: A SharePoint-Style Intranet on Three Backends

In progress

A data-driven intranet that runs identically from memory, the browser, or a real Microsoft 365 tenant — live-tenant verified, a stored XSS found and fixed across 60 sinks, 454 automated checks.

Verified Confirmed against the repo's own docs/STATUS.md 2026-10-21. No live tenant is connected right now.

m365 iam security

At a glance

Problem
A SharePoint-style intranet usually can't be demoed, reviewed, or iterated on without an actual Microsoft 365 tenant sitting behind it — expensive, slow to set up, and risky to experiment against directly.
What I built
One data interface with three interchangeable backends — in-memory, the browser's own storage, and a real SharePoint tenant — so the same UI runs instantly for review, and for real against Microsoft 365 when it matters.
Evidence
~4,700 lines of application code; 454 automated Playwright checks passing; a live Microsoft 365 tenant verified (sign-in, list reads/writes, a document library, a real Power Automate flow, real persona derivation from Entra group membership); a stored XSS found and fixed across 60 sinks, reproduced on 6 real surfaces.
Status
In progress. No live tenant is connected right now

Verified Confirmed directly against the repo's own docs/STATUS.md as of 2026-10-21 (repo unchanged since its 2026-10-16 commit, so this is current, not stale): 3 backends, ~4,700 lines of app code, 454 passing Playwright checks. A live Microsoft 365 tenant was verified in September 2026 (sign-in, list reads/writes, a document library, a real Power Automate flow, real Entra-group persona derivation), confirmed directly by Michael — the trial subscription lapsed shortly after, and no live tenant is connected right now. Everything built in October (the F1-F10 fixes, the timesheet module) came after the trial lapsed, so none of it has been re-verified live. The stored-XSS fix (60 sinks) was verified by reproducing the exploit across 6 real surfaces.

The problem

Building something that's supposed to behave like SharePoint usually means either mocking SharePoint badly, or paying for a tenant before you've written a line of UI code. Neither is a good way to iterate. I wanted a single codebase where the exact same interface could run instantly from memory for day-to-day development, persist locally in a browser for a closer-to-real demo, and — without changing a line of UI code — talk to an actual Microsoft 365 tenant when it was time to prove it for real.

What I built

One provider interface, three backends The UI talks to one provider interface, which is implemented three interchangeable ways: in-memory, browser storage via IndexedDB, or a real SharePoint tenant over REST and Graph. UI Provider interface Memory IndexedDB SharePoint REST + Graph
The UI talks to one provider interface, implemented three interchangeable ways: in-memory, browser storage via IndexedDB, or a real SharePoint tenant over REST and Graph.

One provider interface sits behind the entire UI — lists, a document library, a command palette, a Workflow Builder, Teams-style channels, a timesheet module with manager approvals. Three backends implement that interface: memory (nothing persists, instant), local (persists in the browser via IndexedDB), and sharepoint (a real tenant, via MSAL sign-in and the SharePoint REST API plus Microsoft Graph for identity). Switching backends doesn't change the UI code at all — the same bugs show up (or don't) regardless of which one is running, which is what let the stored-XSS bug below get caught in the first place.

MSAL handles sign-in interactively, with no client secret ever required — vendored locally at a pinned exact version rather than imported from a floating CDN tag, specifically so the authentication code can't change without a commit to this repo. A real Power Automate flow was built and tested against the live tenant: a recurrence trigger, real day-count math, and a real write-back onto a live list item.

Decisions and trade-offs

What went wrong, and what I changed

On 2026-10-10, I found a real stored XSS: a list-rendering file interpolated an item's field straight into innerHTML with no escaping. A list item saved with an <img src=x onerror="..."> payload as a field value would run that script the moment the list rendered — for anyone who saw it. That matters more than a typical demo bug: the SharePoint provider's session token lives in the same browser a planted item would execute in, so under a live tenant this is the textbook stored-XSS-to-account-compromise chain, not a cosmetic issue.

I fixed it across all 60 innerHTML/insertAdjacentHTML sinks that existed in the app at the time — not just the one file where it was found, since the same unescaped pattern was repeated elsewhere. I didn't trust that fix just because the tests passed afterward: I reproduced the exact exploit payload and confirmed it was neutralized across 6 independently real surfaces — a list item, a Workflow Builder entry, an access request, a command-palette search result, a submitted form's own values, and a reflected route name. A Content Security Policy went in as defense in depth right after, not as a substitute for the fix itself.

What's verified, and what isn't

What I'd do next

Links

Keep reading

Next: Cyber Home Lab: Identity Controls in a Live Tenant, Audited Against Themselves