Fix security bugs and migrate image uploads to /api/v1/attachments

* Replace /api/v1/resources with /api/v1/attachments for image uploads
* Upload attachments as JSON with base64-encoded content field
* After memo creation, link each attachment via PATCH /api/v1/attachments/{id}
* Rewrite markdown image URLs to use /file/attachments/{id} pattern
* Fix XSS: sanitize marked.parse output with a DOM-based allowlist sanitizer
* Fix SSRF: validate img.src scheme (http/https only) before fetching
* Fix stack overflow: use chunked base64 encoding for large images
* Update CLAUDE.md to document new attachment flow
This commit is contained in:
2026-03-18 17:55:04 +01:00
parent 42d9f336b1
commit 84b3dd69f1
11 changed files with 274 additions and 119 deletions

View File

@@ -28,16 +28,10 @@
<!-- ── Main editor ── -->
<div id="view-main" class="view hidden">
<header class="flex items-center justify-between p-3 border-b border-gray-100 bg-white sticky top-0 z-10">
<header class="flex items-center justify-between p-3 border-b border-gray-100 dark:border-gray-800 bg-white dark:bg-[#1a1a1f] sticky top-0 z-10">
<div class="logo flex items-center space-x-2 text-emerald-500">
<svg viewBox="0 0 32 32" fill="none" width="18" height="18" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="currentColor"/>
<path d="M11 9C11 8.44772 11.4477 8 12 8H20C20.5523 8 21 8.44772 21 9V23C21 23.5523 20.5523 24 20 24H12C11.4477 24 11 23.5523 11 23V9Z" fill="white"/>
<path d="M14 12H18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M14 15H18" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M14 18H16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
<span id="page-title" class="page-title font-semibold text-gray-800 truncate max-w-[180px]">Clip to Memos</span>
<img src="icons/icon32.png" width="18" height="18" alt="Memos Logo">
<span id="page-title" class="page-title font-semibold text-gray-800 dark:text-gray-200 truncate max-w-[180px]">Clip to Memos</span>
</div>
<div class="header-actions flex space-x-1">
<button id="mode-toggle" class="icon-btn p-1.5 rounded hover:bg-gray-100 text-gray-500 transition" title="Switch clip mode">
@@ -62,14 +56,14 @@
</div>
<!-- edit panel -->
<div id="tab-edit" class="tab-panel flex flex-col h-96 border-b border-gray-100">
<textarea rows="14" id="md-editor" spellcheck="false" placeholder="Markdown content…" class="flex-1 w-full p-3 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-emerald-100"></textarea>
<div id="tab-edit" class="tab-panel flex flex-col h-96 border-b border-gray-100 dark:border-gray-800">
<textarea rows="14" id="md-editor" spellcheck="false" placeholder="Markdown content…" class="flex-1 w-full p-3 text-sm resize-none focus:outline-none focus:ring-1 focus:ring-emerald-100 dark:focus:ring-emerald-900/30 bg-transparent text-inherit"></textarea>
<div id="char-counter" class="char-counter text-right px-3 py-1 text-[10px] text-gray-400">0 chars</div>
</div>
<!-- preview panel -->
<div id="tab-preview" class="tab-panel hidden h-64 overflow-y-auto p-3 border-b border-gray-100">
<div id="md-preview" class="preview-body prose prose-sm max-w-none"></div>
<div id="tab-preview" class="tab-panel hidden h-64 overflow-y-auto p-3 border-b border-gray-100 dark:border-gray-800">
<div id="md-preview" class="preview-body prose prose-sm dark:prose-invert max-w-none"></div>
</div>
<!-- images section -->
@@ -90,16 +84,16 @@
</div>
<!-- footer -->
<footer class="flex items-center justify-between p-3 bg-gray-50">
<footer class="flex items-center justify-between p-3 bg-gray-50 dark:bg-[#22222a]">
<div class="footer-left">
<select id="visibility-select" class="text-xs border border-gray-200 rounded px-2 py-1 bg-white focus:outline-none focus:ring-1 focus:ring-emerald-400 transition">
<select id="visibility-select" class="text-xs border border-gray-200 dark:border-gray-700 rounded px-2 py-1 bg-white dark:bg-[#1a1a1f] text-inherit focus:outline-none focus:ring-1 focus:ring-emerald-400 transition">
<option value="PRIVATE">🔒 Private</option>
<option value="PROTECTED">🔗 Protected</option>
<option value="PUBLIC">🌐 Public</option>
</select>
</div>
<div class="footer-right flex space-x-2">
<button id="reload-btn" class="secondary-btn p-1.5 border border-gray-200 rounded hover:bg-white transition text-gray-500" title="Re-clip page"></button>
<button id="reload-btn" class="secondary-btn p-1.5 border border-gray-200 dark:border-gray-700 rounded hover:bg-white dark:hover:bg-gray-800 transition text-gray-500" title="Re-clip page"></button>
<button id="send-btn" class="send-btn bg-emerald-500 hover:bg-emerald-600 text-white px-4 py-1.5 rounded shadow flex items-center space-x-2 transition">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
<line x1="22" y1="2" x2="11" y2="13"/>