diff --git a/client/vnc/server/input_windows.go b/client/vnc/server/input_windows.go index ab3f8b891..418e5abf8 100644 --- a/client/vnc/server/input_windows.go +++ b/client/vnc/server/input_windows.go @@ -4,6 +4,7 @@ package server import ( "runtime" + "slices" "sync" "unsafe" @@ -559,6 +560,7 @@ var ( procGlobalAlloc = kernel32.NewProc("GlobalAlloc") procGlobalLock = kernel32.NewProc("GlobalLock") + procGlobalSize = kernel32.NewProc("GlobalSize") procGlobalUnlock = kernel32.NewProc("GlobalUnlock") procGlobalFree = kernel32.NewProc("GlobalFree") ) @@ -673,13 +675,30 @@ func (w *WindowsInputInjector) GetClipboard() string { return "" } + // Bounded by the block's own size. Whoever set the clipboard decides + // what is in it, including whether it ends in a NUL, and this runs as + // SYSTEM: scanning for a terminator that is not there reads past the + // allocation into the agent's own memory and hands that to the viewer. + size, _, _ := procGlobalSize.Call(hData) + if size < 2 { + return "" + } ptr, _, _ := procGlobalLock.Call(hData) if ptr == 0 { return "" } defer logCleanupCallArgs("GlobalUnlock", procGlobalUnlock, hData) - return windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ptr))) + return utf16UpToNUL(unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), size/2)) +} + +// utf16UpToNUL decodes units up to the first NUL, or all of them when there +// is none. +func utf16UpToNUL(units []uint16) string { + if i := slices.Index(units, 0); i >= 0 { + units = units[:i] + } + return windows.UTF16ToString(units) } var _ InputInjector = (*WindowsInputInjector)(nil) diff --git a/client/vnc/server/input_windows_clipboard_test.go b/client/vnc/server/input_windows_clipboard_test.go new file mode 100644 index 000000000..abb606425 --- /dev/null +++ b/client/vnc/server/input_windows_clipboard_test.go @@ -0,0 +1,19 @@ +//go:build windows + +package server + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Clipboard text is set by whichever local process owns the clipboard, and +// nothing makes it end in a NUL. Decoding must stop at the block's end, not +// run on looking for a terminator. +func TestUTF16UpToNUL(t *testing.T) { + assert.Equal(t, "hi", utf16UpToNUL([]uint16{'h', 'i', 0, 'x'}), "stops at the first NUL") + assert.Equal(t, "hi", utf16UpToNUL([]uint16{'h', 'i'}), "an unterminated block decodes to its end and no further") + assert.Equal(t, "", utf16UpToNUL([]uint16{0, 'x'}), "a leading NUL is an empty string") + assert.Equal(t, "", utf16UpToNUL(nil)) +}