Skip to content

feat: image paste (Ctrl+V) and drag-and-drop into terminal#84

Merged
Ark0N merged 2 commits into
Ark0N:masterfrom
aakhter:feat/image-paste-and-drop
May 17, 2026
Merged

feat: image paste (Ctrl+V) and drag-and-drop into terminal#84
Ark0N merged 2 commits into
Ark0N:masterfrom
aakhter:feat/image-paste-and-drop

Conversation

@aakhter
Copy link
Copy Markdown
Contributor

@aakhter aakhter commented May 15, 2026

Summary

Lets users paste or drop images into the terminal. The image is saved to ${workdir}/.claude-images/ and the absolute file path is inserted at the cursor — Claude (or any other CLI that reads file paths from stdin) can then process the image without the user having to manually save and @-mention it.

Two entry points:

  • Ctrl+V with an image on the clipboard
  • Drag-and-drop of one or more image files onto the terminal pane (with a visual overlay while dragging)

Mixed-content paste (image + text together) works — both the image is saved as a file and the text is forwarded to the terminal.

Implementation

  • POST /api/sessions/:id/paste-image — hand-parsed multipart endpoint (no @fastify/multipart dependency). Saves under ${session.workingDir}/.claude-images/img-${ts}.${ext} and returns the absolute path.
  • src/web/public/image-input.js — new mixin that wires both paste and drop into the existing terminal:
    • Paste trap technique: Ctrl+V is intercepted at the xterm keyboard layer, focus is briefly redirected to a hidden contenteditable div which receives the paste event (this is the only reliable way to capture clipboard image data over plain HTTP, where the async Clipboard API requires a secure context). Both image bytes and text data from the same paste are extracted, then focus returns to xterm.
    • Drag-and-drop: handlers on the terminal container show a dashed-border overlay during drag and accept any image/* file on drop.
  • src/web/routes/session-routes.ts — multipart parser (~100 lines) and the route handler. Body length is bounded to 10 MB and the image MIME type is validated against an allowlist (PNG/JPEG/WebP/GIF).
  • src/web/server.ts — wires image-input.js into the static asset list and adds session-cleanup so .claude-images/ is removed on session destroy (no orphan disk usage).
  • src/web/public/{index.html,styles.css} — script tag + drop-overlay styling.

The .claude-images/ directory is created lazily on first paste and is the only file the feature writes — no DB rows, no global state.

Test plan

  • Copy an image from a screenshot tool / browser, focus the terminal, press Ctrl+V → an image file appears in ${workdir}/.claude-images/ and its absolute path is typed into the terminal.
  • Drag an image from your file manager onto the terminal → drop overlay appears, file lands in the same directory, path is inserted.
  • Mixed paste (image + text together, e.g. some clipboard managers): the image file is created AND the text is forwarded to the terminal.
  • Multiple images dropped together: each saved with a distinct timestamp and each path inserted.
  • Bogus mime type (text/plain masquerading as image): rejected with 415.
  • >10 MB image: rejected with 413.
  • Delete the session → .claude-images/ directory removed.
  • Plain HTTP (not HTTPS): paste still works (this was a primary motivator — the secure-context-only Clipboard API doesn't help on local LAN setups).

🤖 Generated with Claude Code

Clipboard paste (Ctrl+V) and drag-and-drop of image files into the
terminal. Images are saved to {workdir}/.claude-images/ and the
absolute path is inserted into the terminal input for Claude to read.

- POST /api/sessions/:id/paste-image endpoint (hand-parsed multipart)
- image-input.js mixin with paste trap technique (works on HTTP)
- Ctrl+V intercepted at xterm keyboard level, routes through hidden
  contenteditable div to capture both image and text clipboard data
- Drag-and-drop on terminal container with visual overlay
- Session cleanup deletes .claude-images/ on destroy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ark0N
Ark0N previously requested changes May 17, 2026
Copy link
Copy Markdown
Owner

@Ark0N Ark0N left a comment

Choose a reason for hiding this comment

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

Security review

Verdict: unsafe to merge as-is. Two HIGH-severity issues, four MED, two LOW. Filenames are correctly server-regenerated and auth-gating is intact — the issues below are real but all addressable.

HIGH severity

1. Stored XSS via SVG upload. ALLOWED_IMAGE_EXTS and the Content-Type regex both accept .svg / image/svg+xml (src/web/routes/session-routes.ts:1446, 1513). The saved file lands under session.workingDir/.claude-images/, which is reachable via the existing GET /api/sessions/:id/file-raw?path=.claude-images/<f>.svg route in src/web/routes/file-routes.ts:240-308. That endpoint serves it with Content-Type: image/svg+xml, same-origin. CSP is script-src 'self' 'unsafe-inline' (src/web/middleware/auth.ts:162), so <script> embedded in the SVG executes with full Codeman API access via the session cookie. X-Content-Type-Options: nosniff does not block this — SVG is the declared MIME, not sniffed.

Exploit: attacker (authenticated, or anyone when CODEMAN_PASSWORD is unset and the instance is tunneled) POSTs an SVG containing <script>fetch('/api/sessions',{method:'DELETE'...})</script>, then induces any logged-in admin to open the file via the file browser. Same-origin script → drives every Codeman API.

Fix: drop .svg from the allowlist on session-routes.ts:1446 and the regex on :1513. If SVG must stay, serve it with Content-Disposition: attachment from a separate origin.

2. No CSRF protection. The cookie codeman_session is SameSite=lax (src/web/middleware/auth.ts:135). No Origin/Referer check, no CSRF token, no custom-header requirement on this state-changing endpoint. multipart/form-data is a "simple" CORS request type; a cross-origin <form enctype="multipart/form-data" target="_blank"> POST will carry the session cookie under SameSite=lax.

Exploit: malicious page auto-submits a form to https://<tunnel>/api/sessions/<id>/paste-image with an SVG payload. When an authenticated admin visits the page in another tab, the cookie is attached and the SVG is written. Chains with #1 → one-click full compromise.

Fix: require an Origin header match against an allowlist (or an X-Codeman-CSRF header that <form> cannot send) on state-changing routes.

MED severity

  • No magic-byte validation. Extension and Content-Type are both attacker-supplied; the actual file bytes are never sniffed. Polyglot PNG/HTML files pass. Add a 16-byte header check (PNG 89 50 4E 47, JPEG FF D8 FF, GIF GIF8[79], WebP RIFF…WEBP, BMP BM) and reject mismatches with 415.
  • Symlink write into arbitrary paths. existsSync(imageDir) and mkdirSync(imageDir, {recursive:true}) (session-routes.ts:1538-1540) both follow symlinks. If a process inside workingDir (e.g., the agent itself, or a malicious dep) plants .claude-images → ~/.ssh/, subsequent paste-images write into ~/.ssh/. Use lstat and verify the resolved path is still under session.workingDir — mirror validateSessionFilePath in src/web/route-helpers.ts:59-75.
  • Disk-fill DoS. No rate-limit on /api/sessions/:id/paste-image, no aggregate cap, cleanup only on killMux=true. An authenticated attacker can loop 10 MB POSTs. Add a per-IP token-bucket (e.g., 30/min) and periodic GC of files older than 7d.
  • Hand-rolled multipart parser. indexOf(boundaryBuf) matches the literal anywhere, and the parser hard-codes \r\n\r\n / +2 / -2 offsets — LF-only clients silently corrupt the last byte. Replace with @fastify/multipart, or at minimum anchor the boundary search to \r\n-- and cap the number of parts.

LOW severity

  • Filename collision on same-ms uploads. paste-${Date.now()}${ext} collides under concurrent paste from two tabs; last-write-wins silently. Append randomBytes(8).toString('hex').
  • Bracketed-paste bypass. The Ctrl+V interceptor in src/web/public/terminal-ui.js:86-92 swallows all paste events and re-sends via sendInput(), stripping xterm's bracketed-paste markers. Claude CLI can no longer distinguish typed-vs-pasted input, weakening downstream prompt-injection defences. Return true from customKeyEventHandler when the clipboard contained no image, letting xterm's native paste flow handle text.

What's fine

  • Saved filename is server-regenerated — filename= is only read for .ext, so ../../etc/passwd cannot escape the directory.
  • The streaming 10 MB cap correctly early-aborts rather than post-buffer checking.
  • Auth gating: the route falls under the standard onRequest middleware; no bypass list regression.

Recommended fix order

  1. Drop .svg from the allowlist (session-routes.ts:1446, 1513).
  2. Add Origin/Referer check (or CSRF token) for state-changing routes.
  3. Magic-byte validation against the first chunk before writing.
  4. Symlink check on imageDir via lstat.
  5. Rate-limit + periodic GC of .claude-images/.
  6. Append randomBytes(8) to filenames.
  7. Consider swapping the hand-rolled parser for @fastify/multipart.

Drops .svg / image/svg+xml from the paste-image endpoint. SVGs are
served as image/svg+xml via /api/sessions/:id/file-raw, same-origin,
under a CSP that permits inline scripts — which would execute on view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Ark0N Ark0N dismissed their stale review May 17, 2026 03:53

SVG removed from allowlist (commit a60a9a7) closes the stored-XSS path. CSRF mitigation tracked separately — not blocking merge given current trust model.

@Ark0N Ark0N merged commit 94bcf52 into Ark0N:master May 17, 2026
1 check passed
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.

2 participants