Skip to content

Fix XSS in Markdown renderer + packaging hygiene#2

Open
patricktr wants to merge 3 commits into
victoriaplummer:mainfrom
patricktr:security-hardening
Open

Fix XSS in Markdown renderer + packaging hygiene#2
patricktr wants to merge 3 commits into
victoriaplummer:mainfrom
patricktr:security-hardening

Conversation

@patricktr
Copy link
Copy Markdown

Summary

Two compounding XSS issues in <Markdown> plus a few packaging fixes that prevent the published toolkit from working.

Security fixes (commit 6afabcb)

frontend/components/Markdown.js rendered untrusted cell content via dangerouslySetInnerHTML with two compounding flaws:

  1. Attribute-injection XSS. escapeHtml did not escape \", but captured groups were interpolated into href=\"...\". Payload [x](https://e\" onclick=\"alert(1)) produced <a href=\"https://e\" onclick=\"alert(1)\" ...>.
  2. Unrestricted URL schemes. [click](javascript:alert(1)) produced an executable link; data:text/html,... similarly.

Either lets a base collaborator who can write to a long-text field (or AI output) execute JS in another user's extension iframe and call updateRecordAsync impersonating the victim.

The fix replaces dangerouslySetInnerHTML with structured React-node rendering (so React handles attribute escaping) and allowlists http:/https:/mailto: for both explicit and autolinks. Non-matching schemes render as plain text.

Also fixed a rules-of-hooks violation in the same file (useMemo called after early returns) that crashed the component on empty-to-content transitions, and hardened <AttachmentPreview> to allow only http:/https: image URLs with referrerPolicy=\"no-referrer\".

A new "Security model" section in the README documents the trust boundary and what consumers are responsible for (permission gating before updateRecordAsync, treating AI output as adversarial).

Packaging fixes (commits 32e6dd7, 68f2745)

The published package was broken: main and exports referenced ./src/... but the source lives in ./frontend/. Also:

  • Added files allowlist so npm publish doesn't ship dotfiles or examples
  • sideEffects: false for tree-shaking
  • type: module (the source uses ESM)
  • engines.node and publishConfig.access
  • React peer upper bound (>=17 <20) — the original >=17 was unbounded
  • Fixed repository.url — the previous value pointed at victoriaplummer/airtable-extension-toolkit (without -interface-), which doesn't exist
  • Stale README path references (src/frontend/, skill/SKILL.mdSKILL.md)

Test plan

  • Verify <Markdown> renders the XSS PoCs as plain text / safe links:
    • [click](javascript:alert(1)) → renders as plain text "click"
    • [x](https://e\" onclick=\"alert(1)) → if the URL parses, the \" is React-escaped inside the href attribute
    • https://e\"onerror=\"x → autolink path same as above
  • Verify <Markdown> still renders normal markdown correctly (headings, lists, code, blockquotes, bold/italic, real links).
  • Verify the component no longer crashes when toggling between empty / plain / markdown content (the previous useMemo-after-early-return bug).
  • Verify the package resolves: import { Markdown } from 'airtable-extension-toolkit' and the subpath exports work.

Disclosure note

I considered filing a private security advisory but private vulnerability reporting is currently disabled on this repo. Happy to re-do this as a private advisory if you'd like to enable it (Settings → Code security → "Privately report a vulnerability") — let me know and I'll close this and re-file. Otherwise, since the fix is in the same PR, public disclosure here gives downstream consumers an immediate patch path.

🤖 Generated with Claude Code

patricktr and others added 3 commits May 3, 2026 09:53
The Markdown component rendered untrusted cell content via
dangerouslySetInnerHTML with two compounding flaws:

  1. escapeHtml did not escape ", but captured groups were
     interpolated into href="...". Payload [x](https://e" onclick="x")
     produced a tag with an injected onclick attribute.
  2. URL schemes in links were not validated, so [click](javascript:...)
     produced an executable link.

Either flaw lets a base collaborator who can write to a long-text
field execute JS in another user's extension iframe and call
updateRecordAsync impersonating the victim.

Fix:
  - Replace dangerouslySetInnerHTML with structured React-node
    rendering so React handles attribute escaping.
  - Allowlist http:/https:/mailto: for both explicit links and
    autolinks; non-matching schemes render as plain text.
  - Move useMemo above the early returns to fix a rules-of-hooks
    violation that crashed on empty-to-content transitions.

Also harden AttachmentPreview to allow only http:/https: image URLs
and set referrerPolicy="no-referrer" so embedded images can't leak
referer to attacker-controlled origins.

Document the trust model in the README so consumers know what the
toolkit handles vs. what they must handle themselves.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The published package was broken: main and exports referenced
./src/... but the source lives in ./frontend/. Fix the paths and
add the missing publish-readiness fields:

  - files allowlist so npm publish doesn't ship dotfiles or examples
  - sideEffects: false for tree-shaking
  - type: module (the source uses ESM)
  - engines.node and publishConfig.access
  - React peer upper bound (>=17 <20)
  - repository.url updated to git+https form

Also fix stale path references in the README (src/ -> frontend/,
skill/SKILL.md -> SKILL.md) so the install instructions match
what's actually on disk.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous URL referenced victoriaplummer/airtable-extension-toolkit,
which doesn't exist — the actual repo name is
airtable-interface-extension-toolkit (with -interface-).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant