Skip to content
Back to skills

Security Lit

DSecurity

Frontend security review checklist for an Umbraco backoffice package built with Lit — XSS via `unsafeHTML`/`unsafeSVG`, secrets that leak into a client bundle, sensitive data in browser storage, and npm dependency supply chain. Use when reviewing or writing any Lit template that renders user-generated, AI-generated, or externally-sourced content, any frontend code that reads a build-time environment variable, or any use of `localStorage`/`sessionStorage`. For backend security (Management API ...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 15, 2026
developmenttypescriptrustgoapifrontendbackendsecurity

Works with

  • cli
  • api

Security analysis

D50/100
  • criticalPipes output to a shell interpreter
  • criticalDownloads and executes remote scripts — classic supply chain attack

Pro shows the line behind each finding and how to fix it

Scanned October 15, 2026

npx -y skills add mattbrailsford/umbraco-devkit --skill security-lit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Security Lit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Security Lit
[![Security: D — Skills Directory](https://www.skillsdirectory.com/api/skills/mattbrailsford-security-lit/badge)](https://www.skillsdirectory.com/skills/mattbrailsford-security-lit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: security-lit
description: >-
  Frontend security review checklist for an Umbraco backoffice package built with Lit — XSS via
  `unsafeHTML`/`unsafeSVG`, secrets that leak into a client bundle, sensitive data in browser
  storage, and npm dependency supply chain. Use when reviewing or writing any Lit template that
  renders user-generated, AI-generated, or externally-sourced content, any frontend code that
  reads a build-time environment variable, or any use of `localStorage`/`sessionStorage`. For
  backend security (Management API authz, EF Core injection, secrets storage), see
  `security-dotnet` instead.
---

# Frontend security review for Umbraco backoffice packages

Backend security — Management API authz, EF Core injection, server-side secrets storage, CSRF
for custom endpoints — is `security-dotnet`'s job, not this skill's. Use both together for a
task that touches both layers.

## 🎯 Why: Design for Change

A frontend security gap is just as expensive to unwind as a backend one — sanitizing content
after the fact, once it's already rendered in a dozen places, is far more work than rendering it
safely from the start. Closing gaps by structure (never opting out of escaping, never letting a
secret reach the bundle) keeps the codebase easy to build on; closing them by convention ("the
backoffice is trusted, so it's fine") means the next contributor re-discovers the same gap.

## XSS in Lit templates

Lit's `html` tagged template escapes interpolated values by default — that's the safe path and
needs no special review. The risk is entirely in the directives that opt out of it:
`unsafeHTML()` and `unsafeSVG()` render their argument as raw markup, unescaped.

**Rule:** never pass content through `unsafeHTML`/`unsafeSVG` unless its origin is fully
trusted and it has been sanitized first. This includes AI-generated content, rich-text/RTE
content, and anything pulled from an external API — none of these are trusted just because they
passed through the package's own backend first.

```typescript
// WRONG — rendering AI-generated or RTE content straight through unsafeHTML
render() {
  return html`<div class="preview">${unsafeHTML(this.generatedContent)}</div>`;
}

// RIGHT — sanitize first if real markup genuinely needs to render
render() {
  const safeHtml = DOMPurify.sanitize(this.generatedContent);
  return html`<div class="preview">${unsafeHTML(safeHtml)}</div>`;
}
```

Grep diffs for `unsafeHTML(` and `unsafeSVG(` and trace the argument back to its source. If
that source is a user, an LLM response, an RTE/block-content value, or an external HTTP call,
the missing sanitization step is a finding.

## Secrets never ship in a client bundle

A build-time environment variable inlined into the frontend (`import.meta.env.VITE_*` or
equivalent) ends up as plaintext in the shipped JS, readable by anyone who opens dev tools or
views the bundle — it is not a secret once it's there, no matter how it's named.

**Rule:** anything that needs an API key, a connection string, or a third-party secret must be
called from the backend, with the frontend calling your own Management API instead of the
third-party service directly. If a package's frontend genuinely needs a *public*, low-privilege
key (a maps API key restricted by origin, for instance), that's fine — but treat every
build-time variable as public by default, and justify explicitly the ones that aren't meant to
be.

```typescript
// WRONG — a real provider API key baked into the client bundle at build time
const client = new CoolVendorClient(import.meta.env.VITE_COOL_VENDOR_API_KEY);

// RIGHT — the frontend calls the package's own Management API, which holds the real key
// server-side (see security-dotnet's secrets/sensitive-configuration section)
const response = await fetch("/umbraco/management/api/v1/my-package/vendor-data");
```

## Sensitive data in browser storage

`localStorage` and `sessionStorage` are plain text, readable by any script running on the page
— including a compromised dependency, or a successful XSS elsewhere on the same origin. They
are not a safe place for an auth token, a session identifier, or anything else sensitive.

**Rule:** don't hand-roll token storage in `localStorage`/`sessionStorage` for a package's own
auth or session state. The Umbraco backoffice already handles its own authentication via secure
cookies — lean on that instead of introducing a second, less-safe storage mechanism for the
same concern. Reserve browser storage for genuinely non-sensitive UI conveniences (a collapsed
panel's state, a remembered filter).

## npm dependency / package supply chain

- Commit the lockfile (`package-lock.json`/`pnpm-lock.yaml`) and pin versions deliberately — a
  transitive dependency silently jumping versions on a routine install is exactly how a
  supply-chain compromise reaches a project unnoticed.
- Be wary of a new dependency's `postinstall` script — it runs arbitrary code at install time,
  before any of your own code runs. A well-known, widely-used package doing this is normal; an
  obscure or newly-published one doing it is worth a second look before adding it.
- When a change adds or bumps a package, run `npm audit` (or the package manager's
  equivalent) and treat a known vulnerability in the result as a finding.
- Don't introduce `curl | sh`-style install patterns in build or setup scripts — fetch a pinned
  artifact and verify it instead.

## Findings format

Report findings severity-ordered, most severe first, each with:

- **Location** — `file:line`.
- **What's wrong** — the concrete defect, not a category name.
- **Why it matters** — the actual exploit or exposure, stated plainly, not "this could
  potentially be an issue."

No "consider sanitizing this" hedging — say what must change. The `reviewer` subagent in this
plugin treats an unsanitized `unsafeHTML`/`unsafeSVG` render of untrusted content, or a secret
reaching a client bundle, as an automatic `FAIL`, the same way it treats a backend injection or
authorization gap — these categories are non-negotiable, not judgment calls to weigh against
other findings.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.