Skip to content

Conversation

@raiden-staging
Copy link
Contributor

@raiden-staging raiden-staging commented Jul 18, 2025

(Issue #31)

clipboardv1_test.mp4

Issue Trace

  • Running container logs on clipboard events
9:29PM WRN message handler has failed error="is not the host" event=clipboard/set module=websocket session_id=dummy-vPhbw submodule=handler
  • onkernel/kernel-images:images/chromium-headful/Dockerfile :
FROM ghcr.io/m1k1o/neko/chromium:3.0.6 AS neko
  • m1k1o/neko:server/internal/websocket/handler/clipboard.go | see here
func (h *MessageHandlerCtx) clipboardSet(session types.Session, payload *message.ClipboardData) error {
	if !session.Profile().CanAccessClipboard {
		return errors.New("cannot access clipboard")
	}

	if !session.IsHost() {
		return errors.New("is not the host")
	}
...
  • example member profile config | see here
name: User Name
is_admin: false
...
can_access_clipboard: true # <-----------------------------
...

Suggested Fixes

Alternative A (not tried)

  • in onkernel/kernel-images:images/chromium-headful/client/src/components/connect.vue
...
	// KERNEL: auto-login
	this.$accessor.login({ displayname: 'dummy', password: 'dummy' })
	this.autoPassword = null
  • suggested change would be setting up neko profiles and passing them during build steps
  • note : needs to consider multiplayer user scenarios (user/user and user/agent) for later , and control scopes based on roles , which i assume would be assigned outside of the chromium container and passed to it from the kernel APIs

Alternative B (implemented)

  • added POST /computer/paste : { text } to the Go /server to manage clipboard + trigger paste , enabled CORS to be called by the web client
  • added a global plugin on the vue client to capture clipboard paste events and call the local server

note

  • separate APIs for managing IO seems a good approach for better control by computer use / agent SDKs later [?]
  • important : use WITH_KERNEL_IMAGES_API
    • WITH_KERNEL_IMAGES_API=true ENABLE_WEBRTC=true ./run-docker.sh

TL;DR

Fixed clipboard paste functionality for non-host clients by introducing a new local API endpoint and client-side paste interception.

Why we made these changes

The existing neko clipboard mechanism restricted paste operations to the "host" session, preventing other clients (like the dummy client in the chromium container) from pasting content, as evidenced by the "is not the host" error. This change enables universal paste functionality.

What changed?

  • Server:
    • Added POST /computer/paste endpoint to handle clipboard setting and simulate Ctrl+V (using xclip and xdotool).
    • Enabled CORS for the API server to allow client-side calls.
    • Added go-chi/cors dependency and updated OpenAPI spec.
  • Client:
    • Introduced GlobalPaste Vue plugin to intercept global paste events (Ctrl+V/Cmd+V).
    • Sends intercepted clipboard text to the new local /computer/paste endpoint.
    • Updated video.vue and remote.ts to use the new local fallback service for clipboard updates.
    • Added minor logging and updated VS Code settings.

Validation

Verified with provided video demonstration (Issue #31).

Copy link

@mesa-dot-dev mesa-dot-dev bot left a comment

Choose a reason for hiding this comment

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

What Changed

This PR addresses a clipboard issue where non-host users couldn't paste text. It introduces a new backend API endpoint, POST /computer/paste, which uses xclip and xdotool to perform the paste operation directly on the host system. On the client side, a new Vue plugin (globalPaste.ts) is added to intercept Ctrl+V/Cmd+V events, read the clipboard content, and send it to this new local API endpoint. CORS has also been enabled on the Go server to allow requests from the web client.

Risks / Concerns

The review identified two high-severity security risks:

  1. The CORS policy in server/cmd/api/main.go is overly permissive (AllowedOrigins: "*" with credentials), creating a potential CSRF vulnerability. It should be restricted to specific, trusted origins.
  2. The client-side code in images/chromium-headful/client/src/neko/index.ts logs the full content of the clipboard to the browser console, which could expose sensitive user data.

14 files reviewed | 2 comments | Review on Mesa | Edit Reviewer Settings

}

protected [EVENT.CONTROL.CLIPBOARD]({ text }: ControlClipboardPayload) {
console.log('[clipboard] EVENT.CONTROL.CLIPBOARD:', text)
Copy link

Choose a reason for hiding this comment

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

Consider the security implications of logging clipboard data. Clipboard content could contain sensitive information like passwords, API keys, or personal data. While this logging is helpful for debugging, you may want to either:

  1. Truncate/sanitize the logged text (e.g., console.log('[clipboard] EVENT.CONTROL.CLIPBOARD:', text.substring(0, 50) + '...'))
  2. Add a flag to disable clipboard logging in production
  3. Use a more secure logging method that doesn't expose sensitive data in browser console

This is particularly important since the browser console can be accessed by users or browser extensions.

Type: Security | Severity: High


r := chi.NewRouter()

r.Use(cors.Handler(cors.Options{
Copy link

Choose a reason for hiding this comment

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

Security concern: Using AllowedOrigins: "*" with AllowCredentials: true is a dangerous combination that can lead to CSRF attacks. Any website can make authenticated requests to this API. Consider either:

  1. Remove AllowCredentials: true if credentials aren't needed
  2. Specify exact origins instead of "*" (e.g., []string{"http://localhost:3000"} for the Vue client)
  3. If this is truly localhost-only, consider restricting origins to localhost variants
Suggested change
r.Use(cors.Handler(cors.Options{
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"http://localhost:3000", "http://127.0.0.1:3000"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
ExposedHeaders: []string{"X-Recording-Started-At", "X-Recording-Finished-At"},
AllowCredentials: true,
MaxAge: 300,
}))

Type: Security | Severity: High

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Bug: Clipboard URL Hardcoding and Paste Interception Issues

The commit introduces a hardcoded http://localhost:10001/computer/paste URL in both remote.ts (for fallback clipboard updates) and globalPaste.ts (for Ctrl/Cmd+V interception). This assumes the server runs on localhost:10001, which is problematic as the server's host/port is configurable, leading to connection failures in non-localhost or containerized deployments.

Additionally, the globalPaste plugin has two issues:

  • It unconditionally calls e.preventDefault() for Ctrl/Cmd+V, overriding native browser paste behavior. This breaks standard paste functionality in text inputs and prevents fallback if the custom clipboard read or API call fails.
  • It sends an API request even when navigator.clipboard.readText() returns an empty string, causing unnecessary network traffic and server 400 errors.

images/chromium-headful/client/src/plugins/globalPaste.ts#L8-L19

https://github.com/onkernel/kernel-images/blob/ad20dedac69ff16eecbb93815f9b8a9de80b1f48/images/chromium-headful/client/src/plugins/globalPaste.ts#L8-L19

images/chromium-headful/client/src/store/remote.ts#L45-L46

https://github.com/onkernel/kernel-images/blob/ad20dedac69ff16eecbb93815f9b8a9de80b1f48/images/chromium-headful/client/src/store/remote.ts#L45-L46

Fix in CursorFix in Web


Bug: CORS Misconfiguration: Insecure Wildcard Origin

The CORS configuration is invalid and insecure: AllowedOrigins is set to "*" while AllowCredentials is true. This violates the CORS specification, causing browsers to reject cross-origin requests and creating a security vulnerability by allowing any website to make authenticated requests (e.g., CSRF). The configuration must either set AllowCredentials to false or specify explicit allowed origins.

server/cmd/api/main.go#L45-L53

https://github.com/onkernel/kernel-images/blob/ad20dedac69ff16eecbb93815f9b8a9de80b1f48/server/cmd/api/main.go#L45-L53

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

@juecd juecd requested review from juecd and rgarcia July 18, 2025 03:40
Copy link
Contributor

@rgarcia rgarcia left a comment

Choose a reason for hiding this comment

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

I think "Alternative A" is more straightforward, i.e. giving the "dummy" user a profile that allows clipboard use in neko

try {
const text = await navigator.clipboard.readText()
console.log(`[vue:globalPaste]:payload:` , text)
await fetch('http://localhost:10001/computer/paste', {
Copy link
Contributor

Choose a reason for hiding this comment

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

using localhost here won't work in prod

Copy link
Contributor Author

Choose a reason for hiding this comment

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

on it 👍

note :
idea of external controls (outside of neko events) exposed via API might be useful down the line ; as a controller for agents to stream events to/from via sockets. not via localhost on the instance like in here - would be routed to and token-gated by the kernel api , and connected to by the ^ container and user/agent outside of the vue app

Copy link
Contributor

Choose a reason for hiding this comment

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

yes definitely--this is why we added the click/move mouse stuff into the API. I think eventually we will go deeper on that front

try {
const text = await navigator.clipboard.readText()
console.log(`[vue:globalPaste]:payload:` , text)
await fetch('http://localhost:10001/computer/paste', {
Copy link

Choose a reason for hiding this comment

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

Bug: Hardcoded URL Causes Production Environment Issues

The http://localhost:10001/computer/paste URL is hardcoded in remote.ts and globalPaste.ts. This prevents the application from functioning correctly in production, containerized, or distributed environments where the API server is not running on localhost:10001. This issue was previously identified in PR discussions.

Locations (2)

Fix in Cursor Fix in Web

ExposedHeaders: []string{"X-Recording-Started-At", "X-Recording-Finished-At"},
AllowCredentials: true,
MaxAge: 300,
}))
Copy link

Choose a reason for hiding this comment

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

Bug: CORS Misconfiguration Leads to Security Vulnerability

The CORS configuration is overly permissive, allowing all origins (*) while simultaneously setting AllowCredentials: true. This combination is invalid according to the CORS specification and creates a significant security vulnerability, enabling cross-origin credential theft and attacks.

Locations (1)

Fix in Cursor Fix in Web

@raiden-staging raiden-staging mentioned this pull request Jul 28, 2025
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