Skip to content

fix: prevent markdown link javascript URL bypass in description rendering - #271

Open
ikuradon wants to merge 1 commit into
mainfrom
codex/fix-xss-vulnerability-in-markdown-rendering
Open

fix: prevent markdown link javascript URL bypass in description rendering#271
ikuradon wants to merge 1 commit into
mainfrom
codex/fix-xss-vulnerability-in-markdown-rendering

Conversation

@ikuradon

Copy link
Copy Markdown
Owner

Motivation

  • Close a critical XSS regression where untrusted RSS/Nostr descriptions converted to Markdown could produce clickable anchors whose href was interpreted as javascript: after browser URL canonicalization (e.g. java\tscript: with embedded control characters).

Description

  • Hardened isSafeUrl in src/shared/utils/html.ts to require an explicit scheme, canonicalize the URL with new URL(...), and allow only http: and https: protocols.
  • Added a scheme-presence regex check to reject non-absolute/malformed links before parsing.
  • Kept existing renderMarkdown behavior (it still escapes raw HTML and constructs anchors), but now anchors are created only when isSafeUrl returns true.
  • Added regression tests in src/shared/utils/html.test.ts to cover obfuscated javascript: payloads and data: URLs to prevent reintroducing the bypass.

Testing

  • Added unit tests to src/shared/utils/html.test.ts that assert renderMarkdown does not emit href for javascript: (including control-character-obfuscated variants) and data: URLs.
  • Attempted to run pnpm vitest src/shared/utils/html.test.ts, but the environment could not execute the suite because the repository requires Node >=24.0.0 while the runtime provided Node v20.20.2, so tests were not executed here.

Codex Task

@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.16%. Comparing base (8611c0b) to head (cc5cc6d).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/shared/utils/html.ts 75.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #271      +/-   ##
==========================================
- Coverage   80.16%   80.16%   -0.01%     
==========================================
  Files         235      235              
  Lines       12617    12622       +5     
  Branches     3476     3478       +2     
==========================================
+ Hits        10115    10118       +3     
- Misses       1221     1222       +1     
- Partials     1281     1282       +1     
Files with missing lines Coverage Δ
src/shared/utils/html.ts 90.00% <75.00%> (-10.00%) ⬇️

@codecov

codecov Bot commented May 23, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 4.53MB (9.85%) ⬆️⚠️, exceeding the configured threshold of 5%.

Bundle name Size Change
resonote-__sveltekit_1o3ds9l.app-esm 2.27MB 2.27MB (100%) ⬆️⚠️
resonote-__sveltekit_1gdumn2.app-esm 2.27MB 2.27MB (100%) ⬆️⚠️

@greptile-apps

greptile-apps Bot commented May 23, 2026

Copy link
Copy Markdown

Greptile Summary

XSS リグレッションを修正する PR です。isSafeUrl をデニーリスト方式(javascript:/data:/vbscript: の前方一致拒否)からアローリスト方式(http:/https: のみ許可)に刷新し、new URL() による正規化と事前スキーム正規表現チェックで制御文字入り難読化 URL を遮断します。

  • isSafeUrl に RFC 3986 スキーム正規表現と new URL() による正規化を追加し、java\ script: のような埋め込み制御文字による javascript: 偽装をブロック。
  • data: URL および制御文字入り javascript: URL に対するリグレッションテストを html.test.ts に追加。
  • 副作用として、相対 URL(/path#anchor 等)が従来の動作ではクリック可能リンクとして出力されていたが、新実装ではプレーンテキストになる。

Confidence Score: 4/5

セキュリティ修正として有効で、XSS 遮断ロジックは堅牢です。相対 URL の挙動変更は意図的と思われますが、コメントとテストで明示されていないため確認が必要です。

アローリスト方式と new URL() 正規化の組み合わせは制御文字による難読化を確実に遮断しており、コアのセキュリティ修正は正しく実装されています。一方、相対 URL が従来は href として出力されていたのに対し、新コードではプレーンテキストになるという動作変更が文書化もテストもされていません。

src/shared/utils/html.ts の isSafeUrl 周辺(相対 URL の挙動変更の意図確認)と src/shared/utils/html.test.ts(相対 URL の新挙動を検証するテストの追加)に注目してください。

Important Files Changed

Filename Overview
src/shared/utils/html.ts isSafeUrl をデニーリストからアローリストに変更。http/https のみ許可し、new URL() で正規化。制御文字入り難読化スキームはスキーム正規表現で遮断。相対 URL (/ や # 始まり) が従来は許可されていたが新コードでは全拒否される動作変更あり。
src/shared/utils/html.test.ts 制御文字入り javascript: URL (java\tscript:) と data: URL のリグレッションテストを追加。既存テストへの影響なし。

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["isSafeUrl(url)"] --> B{"url.trim() が空?"}
    B -- Yes --> FAIL["return false"]
    B -- No --> C{"スキーム正規表現\n一致?"}
    C -- No --> FAIL
    C -- Yes --> D["new URL(trimmed)"]
    D -- "例外" --> FAIL
    D -- "成功" --> E{"protocol が\nhttp: か https:?"}
    E -- No --> FAIL
    E -- Yes --> PASS["return true"]
Loading

Comments Outside Diff (1)

  1. src/shared/utils/html.ts, line 77-81 (link)

    P2 相対 URL の挙動変更: 従来の実装では /path#anchor./foo のような相対 URL は javascript:/data:/vbscript: で始まらないためそのまま href として出力されていましたが、新実装ではスキーム正規表現に一致しないため無条件に false を返します。RSS/Nostr のコンテンツで相対リンクが使われていた場合、クリック可能なリンクが無音でプレーンテキストに変わります。意図的な変更であれば、renderMarkdown の JSDoc コメントにその旨を明記し、挙動を確認するテストケースを追加することを推奨します。

Reviews (1): Last reviewed commit: "fix: harden markdown link URL protocol v..." | Re-trigger Greptile

Comment on lines +92 to 97
it('blocks data: URLs', () => {
const result = renderMarkdown('[xss](data:text/html;base64,PHNjcmlwdD4=)');
expect(result).not.toContain('href');
});

it('escapes HTML in link text', () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 相対 URL が従来許可されていたことを踏まえ、新しい挙動(相対 URL がプレーンテキスト化される)を明示的に検証するテストケースを追加することを推奨します。意図せぬリグレッションを防ぐためのドキュメント的な役割も果たします。

Suggested change
it('blocks data: URLs', () => {
const result = renderMarkdown('[xss](data:text/html;base64,PHNjcmlwdD4=)');
expect(result).not.toContain('href');
});
it('escapes HTML in link text', () => {
it('blocks data: URLs', () => {
const result = renderMarkdown('[xss](data:text/html;base64,PHNjcmlwdD4=)');
expect(result).not.toContain('href');
});
it('renders relative URLs as plain text (no href)', () => {
expect(renderMarkdown('[link](/path)')).not.toContain('href');
expect(renderMarkdown('[anchor](#section)')).not.toContain('href');
expect(renderMarkdown('[rel](./foo)')).not.toContain('href');
});
it('escapes HTML in link text', () => {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant