Skip to content

Clipboard.GetData performs improper validation #13929

@kevingosse

Description

@kevingosse

.NET version

.NET 9

Did it work in .NET Framework?

Not tested/verified

Did it work in any of the earlier releases of .NET Core or .NET 5+?

No response

Issue description

I ran into a crash caused by an AccessViolationException with the following callstack:

at System.String.Ctor(SByte*)
   at System.Windows.Forms.DataObject+Composition+NativeToWinFormsAdapter.<GetDataFromHGLOBAL>g__ReadStringFromHGLOBAL|11_0(Windows.Win32.Foundation.HGLOBAL, Boolean)
   at System.Windows.Forms.DataObject+Composition+NativeToWinFormsAdapter.GetDataFromHGLOBAL(Windows.Win32.Foundation.HGLOBAL, System.String)
   at System.Windows.Forms.DataObject+Composition+NativeToWinFormsAdapter.<GetObjectFromDataObject>g__TryGetHGLOBALData|12_1(Windows.Win32.System.Com.IDataObject*, System.String, Boolean ByRef)
   at System.Windows.Forms.DataObject+Composition+NativeToWinFormsAdapter.GetObjectFromDataObject(Windows.Win32.System.Com.IDataObject*, System.String, Boolean ByRef)
   at System.Windows.Forms.DataObject+Composition+NativeToWinFormsAdapter.System.Windows.Forms.IDataObject.GetData(System.String, Boolean)
   at System.Windows.Forms.DataObject+Composition.System.Windows.Forms.IDataObject.GetData(System.String, Boolean)
   at System.Windows.Forms.DataObject.GetData(System.String, Boolean)
   at System.Windows.Forms.DataObject.GetData(System.String)

After investigation, it turns out that another application stored a badly formatted string in the clipboard (the null-terminator was missing), and Clipboard.GetData converts the data into a string without validation or size limit. It directly gives the pointer to the string constructor, which will read the memory past the limit until it finds a random \0 or triggers an access violation (whichever comes first).

The clipboard API should either:

  • Limit the size of the string with a call to GlobalSize (I believe this is what most applications do)
  • Inspect the buffer and throw an exception if it contains no null-terminator

AccessViolationException is not catchable, so applications that use Clipboard.GetData have currently no way to protect themselves from malformed data.

Steps to reproduce

Sample app:

using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;

namespace ClipboardCorruptionTest;

internal class Program
{
    [STAThread]
    static void Main(string[] args)
    {
        if (args.Length >= 2)
        {
            if (args[0] == "set")
            {
                var nullTerminator = args[1] == "true";

                var rtfString = @"{\rtf1\ansi\deff0{\fonttbl{\f0 Arial;}}\fs24 Bad-RTF}";

                SetClipboard(rtfString, nullTerminator);

                Console.WriteLine($"Stored a string of length {rtfString.Length} with nullTerminator={nullTerminator}");
            }

            return;
        }

        // Create some memory traffic
        var ptrs = new nint[100000];
        var random = new Random();
        for (int i = 0; i < 100000; i++)
        {
            var size = random.Next(1, 1000);
            ptrs[i] = Marshal.AllocHGlobal(size);
            new Span<byte>((void*)ptrs[i], size).Fill((byte)'x');            
        }

        foreach (var ptr in ptrs)
        {
            Marshal.FreeHGlobal(ptr);
        }

        bool hasRtf = Clipboard.ContainsText(TextDataFormat.Rtf);

        if (!hasRtf)
        {
            Console.WriteLine("No RTF data on clipboard");
            return;
        }

        var result = Clipboard.GetText(TextDataFormat.Rtf);
        
        Console.WriteLine($"Retrieved RTF data of length {result.Length}");
        Console.WriteLine(result);
    }

    private static void SetClipboard(string rtfString, bool nullTerminator)
    {
        var rtfBytes = Encoding.ASCII.GetBytes(rtfString);

        const uint GMEM_MOVEABLE = 0x0002;

        uint cfRtf = RegisterClipboardFormat(DataFormats.Rtf);
        if (cfRtf == 0) throw new Win32Exception(Marshal.GetLastWin32Error(), "RegisterClipboardFormat failed.");

        if (!OpenClipboard(IntPtr.Zero))
            throw new Win32Exception(Marshal.GetLastWin32Error(), "OpenClipboard failed.");

        IntPtr hMem;

        try
        {
            if (!EmptyClipboard())
                throw new Win32Exception(Marshal.GetLastWin32Error(), "EmptyClipboard failed.");

            var length = (UIntPtr)rtfBytes.Length;

            if (nullTerminator)
            {
                length += 1;
            }

            hMem = GlobalAlloc(GMEM_MOVEABLE, length);
            if (hMem == IntPtr.Zero)
                throw new Win32Exception(Marshal.GetLastWin32Error(), "GlobalAlloc failed.");

            IntPtr p = GlobalLock(hMem);
            if (p == IntPtr.Zero)
                throw new Win32Exception(Marshal.GetLastWin32Error(), "GlobalLock failed.");

            try
            {
                Marshal.Copy(rtfBytes, 0, p, rtfBytes.Length);
            }
            finally
            {
                GlobalUnlock(hMem);
            }

            if (SetClipboardData(cfRtf, hMem) == IntPtr.Zero)
            {
                int err = Marshal.GetLastWin32Error();
                GlobalFree(hMem);
                throw new Win32Exception(err, "SetClipboardData(RTF) failed.");
            }
        }
        finally
        {
            CloseClipboard();
        }
    }

    [DllImport("user32.dll", SetLastError = true)]
    static extern bool OpenClipboard(IntPtr hWndNewOwner);
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool CloseClipboard();
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GlobalLock(IntPtr hMem);
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool GlobalUnlock(IntPtr hMem);
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GlobalAlloc(uint uFlags, UIntPtr dwBytes);
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern IntPtr GlobalFree(IntPtr hMem);
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    static extern uint RegisterClipboardFormat(string lpszFormat);
    [DllImport("user32.dll", SetLastError = true)]
    static extern IntPtr SetClipboardData(uint uFormat, IntPtr hMem);
    [DllImport("user32.dll", SetLastError = true)]
    static extern bool EmptyClipboard();
}

Run the app with arguments set true or set false to put a string in the clipboard, respectively with or without a null terminator. Then run without arguments to read it.

> ClipboardTest.exe set true
Stored a string of length 53 with nullTerminator=True

> ClipboardTest.exe
Retrieved RTF data of length 53
{\rtf1\ansi\deff0{\fonttbl{\f0 Arial;}}\fs24 Bad-RTF}

> ClipboardTest.exe set false
Stored a string of length 53 with nullTerminator=False

> ClipboardTest.exe
Retrieved RTF data of length 64
{\rtf1\ansi\deff0{\fonttbl{\f0 Arial;}}\fs24 Bad-RTF}xxxxxxxxxxx

Since the memory is read out of bounds, the results are random. It can take a few tries to see the garbage.

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions