Skip to content

Clipboard.GetData performs improper validation #13929

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions