OLE clipboard supports standard exchange formats and additionally allows users to register custom formats for data exchange. Although standard exchange formats do not use binary format as they are defined by Windows, custom format serialization of objects in .NET has used the BinaryFormatter (in both WinForms and WPF).
WinForms Clipboard falls back to the BinaryFormatter when consuming custom types stored as custom clipboard formats. We propose a safer set of Clipboard and DataObject APIs that restrict unbounded BinaryFormatter deserialization to types known at the compile time and provide an alternative serialization method, JSON, for user types.
API Proposal
We propose the same API changes in WinForms and WPF assemblies. The proposed APIs will support our updated Clipboard and Drag/Drop guidance which is:
To avoid BinaryFormatter, use intrinsically handled types, which include primitive types, arrays or lists of primitives, exchange types from System.Drawing.Primitives.dll (PointF, RectangleF, Point, Rectangle, SizeF, Size, Color), and types commonly used in WinForms applications, such as System.Drawing.Bitmap. These types are handled for the user and continue to work without migration steps needed. We are open to adding more intrinsically handled types as needed.
.NET types that contain only public properties or fields and avoid inheritance, is the recommended type for data exchange in general, including in the Clipboard scenarios. Such types do not require BinaryFormatter with the proposed APIs, but might require custom JSON serialization. This is the recommended way for new application to use the Clipboard or Drag/Drop.
If this is not feasible for user's data transfer type, it is recommended to use JSON to format user type as byte[] or string. Adjustment will need to be made on the consuming side to handle receiving a JSON formatted type.
If exchange type can't be updated, the new consumption APIs will allow the user to restrict BinaryFormatter deserialization to allowed types. This usage is acceptable during migration off BinaryFormatter and is not secure.
Use the obsoleted APIs and opt into the BinaryFormatter only when other approaches are not applicable.
namespace System.Windows.Forms;
and
namespace System.Windows;
public static partial class Clipboard
{
+ // Saves the data onto the clipboard in the specified format using System.Text.Json. Will throw InvalidOperationException if DataObject is passed as it is ambiguous what user is intending given DataObject cannot be meaningfully JSON serialized.
+ public static void SetDataAsJson<T>(string format, T data) { }
+ [Obsolete("`Clipboard.GetData(string)` method is obsolete. Use `Clipboard.TryGetData<T>` instead.", false, DiagnosticId = "WFDEV005", UrlFormat = "https://aka.ms/winforms-warnings/{0}")
+ [EditorBrowsable(EditorBrowsableState.Never)]
public static object? GetData(string format) { }
+ // Verifies that payload contains type T and then attempts to read or deserialize it.
+ public static bool TryGetData<T>(string format, out T data) { }
+ // Uses user-provided resolve to match the requested type to the payload content and to rehydrate the payload.
+ public static bool TryGetData<T>(
string format, Func<Reflection.Metadata.TypeName, Type> resolver, out T data) { }
}
Managed implementation of OLE's IDataObject definition and is being used in all OLE operations for WinForms/WPF.
namespace System.Windows.Forms;
and
namespace System.Windows;
public partial class DataObject : IDataObject, Runtime.InteropServices.ComTypes.IDataObject
{
+ // Stores the specified data and its associated format in this instance using System.Text.Json. Will throw InvalidOperationException if DataObject is passed as it is ambiguous what user is intending given DataObject cannot be meaningfully JSON serialized.
+ public void SetDataAsJson<T>(T data) { }
+ public void SetDataAsJson<T>(string format, T data) { }
+ public void SetDataAsJson<T>(string format, bool autoConvert, T data) { }
+ [Obsolete("`DataObject.GetData` methods are obsolete. Use the corresponding `DataObject.TryGetData<T>` instead.", false, DiagnosticId = "WFDEV005", UrlFormat = "https://aka.ms/winforms-warnings/{0}")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
public virtual object? GetData(string format, bool autoConvert) { }
+ [Obsolete("`DataObject.GetData` methods are obsolete. Use the corresponding `DataObject.TryGetData<T>` instead.", false, DiagnosticId = "WFDEV005", UrlFormat = "https://aka.ms/winforms-warnings/{0}")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
public virtual object? GetData(string format) { }
+ [Obsolete("`DataObject.GetData` methods are obsolete. Use the corresponding `DataObject.TryGetData<T>` instead.", false, DiagnosticId = "WFDEV005", UrlFormat = "https://aka.ms/winforms-warnings/{0}")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
public virtual object? GetData(Type format) { }
+ public virtual bool TryGetData<T>(out T data) { }
+ public virtual bool TryGetData<T>(string format, out T data) { }
+ public virtual bool TryGetData<T>(string format, bool autoConvert, out T data) { }
+ public virtual bool TryGetData<T>(string format, Func<Reflection.Metadata.TypeName, Type> resolver, bool autoConvert, out T data) { }
}
namespace System.Windows.Forms;
and
namespace System.Windows;
public partial interface IDataObject
{
object? GetData(string format, bool autoConvert);
object? GetData(string format);
object? GetData(Type format);
+ // Default implementations of these interface methods are using the existing GetData methods.
+ bool TryGetData<T>(out T data);
+ bool TryGetData<T>(string format, out T data);
+ bool TryGetData<T>(string format, bool autoConvert, out T data);
+ bool TryGetData<T>(string format, Func<Reflection.Metadata.TypeName, Type> resolver, bool autoConvert, out T data);
}
namespace System.Windows.Forms;
public class partial class Control
{
+ // Begins drag operation, storing the drag data using System.Text.Json. Will throw InvalidOperationException if DataObject is passed to have a better error reporting in a common scenario.
+ public DragDropEffects DoDragDropAsJson<T>(T data, DragDropEffects allowedEffects, Bitmap? dragImage, Point cursorOffset, bool useDefaultDragImage)
+ public DragDropEffects DoDragDropAsJson<T>(T data, DragDropEffects allowedEffects)
}
namespace System.Windows;
public static class DragDrop
{
+ // Begins drag operation, storing the drag data using System.Text.Json. Will throw InvalidOperationException if DataObject is passed to have a better error reporting in a common scenario.
+ public static DragDropEffects DoDragDropAsJson<T>(DependencyObject dragSource, T data, DragDropEffects allowedEffects)
}
// VisualBasic wrapper for WinForms Clipboard.
namespace Microsoft.VisualBasic.MyServices
public partial class ClipboardProxy
{
+ public void SetDataAsJson<T>(T data) { }
+ public void SetDataAsJson<T>(string format, T data) { }
+ [Obsolete("`ClipboardProxy.GetData(As String)` method is obsolete. Use `ClipboardProxy.TryGetData(Of T)` instead.", false, DiagnosticId = "WFDEV005", UrlFormat = "https://aka.ms/winforms-warnings/{0}")
+ [EditorBrowsable(EditorBrowsableState.Never)]
public object GetData(string format) { }
+ public bool TryGetData<T>(string format, out T data) { }
+ public bool TryGetData<T>(string format, System.Func<System.Reflection.Metadata.TypeName, System.Type> resolver, out T data) { }
}
New configuration switch:
ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization - controls whether BinaryFormatter is enabled as a fallback for Clipboard and Drag/drop scenarios. By default, it is false.
API Usage
Clipboard API Examples
Before introduction of new APIs
Users would need to enable BinaryFormatter to serialize/deserialize their types.
[Serializable]
public class WeatherForecastPOCO
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
public void SetGetClipboardData()
{
WeatherForecastPOCO weatherForecast = new()
{
Date = DateTime.Parse("2019-08-01"),
TemperatureCelsius = 25,
Summary = "Hot",
};
// BinaryFormatter must be enabled for this to be successful.
Clipboard.SetData("myCustomFormat", weatherForecast);
pragma warning disable WFDEV005 // Type or member is obsolete
if (Clipboard.GetData("myCustomFormat") is WeatherForecast forecast)
pragma warning restore WFDEV005
{
// Do things with forecast.
}
}
2. Users could use manual JSON serialization to avoid opting into the BinaryFormatter with the old APIs.
```c#
public class WeatherForecastPOCO
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
public void SetGetClipboardDataJson()
{
WeatherForecastPOCO weatherForecast = new()
{
Date = DateTime.Parse("2019-08-01"),
TemperatureCelsius = 25,
Summary = "Hot",
};
byte[] serialized = JsonSerializer.SerializeToUtf8Bytes(weatherForecast);
Clipboard.SetData("myCustomFormat", serialized);
#pragma warning disable WFDEV005 // Type or member is obsolete
if (Clipboard.GetData("myCustomFormat") is byte[] byteData)
#pragma warning restore WFDEV005
{
if (JsonSerializer.Deserialize(byteData, typeof(WeatherForecast)) is WeatherForecast forecast)
{
// Do things with forecast.
}
}
}
After introduction of new APIs
No need for users to manually JSON serialize their data and can specify directly in TryGetData what type they are expecting. This code does not require BinaryFormatter.
public class WeatherForecastPOCO
{
public DateTimeOffset Date { get; set; }
public int TemperatureCelsius { get; set; }
public string? Summary { get; set; }
}
public void SetGetClipboardData()
{
WeatherForecastPOCO weatherForecast = new()
{
Date = DateTime.Parse("2019-08-01"),
TemperatureCelsius = 25,
Summary = "Hot",
};
Clipboard.SetDataAsJson("myCustomFormat", weatherForecast);
if (Clipboard.TryGetData("myCustomFormat", WeatherForecast? forecast))
{
// Do things with forecast.
}
}
When exchange types can't be replaced by POCO types, application can restrictBinaryFormatter deserialization by providing a set of allowed types.
// Writer side
using Font value = new("Microsoft Sans Serif", emSize: 10);
Clipboard.SetData("font", value);
// Consumer side
if (Clipboard.TryGetData("font", FontResolver, out Font data))
{
// Do things with data
}
private static Type FontResolver(TypeName typeName)
{
(string name, Type type)[] allowedTypes =
[
(typeof(FontStyle).FullName!, typeof(FontStyle)),
(typeof(FontFamily).FullName!, typeof(FontFamily)),
(typeof(GraphicsUnit).FullName!, typeof(GraphicsUnit)),
];
string fullName = typeName.FullName;
foreach (var (name, type) in allowedTypes)
{
// Namespace-qualified type name.
if (name == fullName)
{
return type;
}
}
throw new NotSupportedException($"Can't resolve {fullName}");
}
#### Drag/Drop Usage Example
No BinaryFormatter is required.
```c#
private Form _form1 = new();
private Control _beingDrag = new();
_beingDrag.MouseDown += beingDrag_MouseDown;
_form1.DragDrop += Form1_DragDrop;
void beingDrag_MouseDown(object sender, MouseEventArgs e)
{
WeatherForecast weatherForecast = new WeatherForecast
{
Date = DateTimeOffset.Now,
TemperatureCelsius = 25,
Summary = "Hot"
};
_beingDrag.DoDragDropAsJson(weatherForecast, DragDropEffects.Copy);
}
void Form1_DragDrop(object sender, DragEventArgs e)
{
DataObject dataObject = e.Data;
if (dataObject.TryGetData(typeof(WeatherForecast), out WeatherForecast? deserialized))
{
// Do things with deserialized data.
}
}
Alternative Designs
Replace a Func<TypeName, Type> with a resolver interface that can be reused in ResX and ActiveX scenarios and potentially in other Runtime scenarios, move the IResolver interface into System.Runtime.Serialization namespace. That would be a replacement for the resolver Func<TypeName, Type>.
bool TryGetData<T>(string format, IResolver resolver, bool autoConvert, out T data);
namespace System;
public interface IResolver
{
Type TypeFromTypeName(TypeName typeName);
}
Should we make the GetData methods in the managed IDataObject interface obsolete?
These methods are implemented by the user, we don’t know if they are vulnerable or not. But they propagate a bad pattern by returning an unconstrained type (System.Object). There might be too many false positives if we obsolete them. The recommended way is for users to derive from the managed DataObject, not to implement the interface from scratch, and that scenario is covered.
Should we make the SetData overloads obsolete?
No, this scenario is not vulnerable, it propagates a bad pattern only when serializing more complex types. We will address this with an analyzer.
Why take T for SetDataAsJson / DoDragDropAsJson APIs instead of object?
We need to save the original type of the data that is being passed in so that we can rehydrate the type when the user asks for it, meaning that we will need to rely on Object.GetType to get the type of the passed in data in SetDataAsJson / DoDragDropAsJson APIs. This is not trim friendly because it might use reflection.
Should an optional parameter to SetData and DoDragDrop APIs to indicate serializing with Json is desired instead of introducing a new API signature?
This is an option, but it is again not trim friendly as we would need to rely on Object.GetType.
Should we make the managed IDataObject interface obsolete?
The shape of the managed interface matches that of the OLE interface, and the whole ecosystem depends on it, so any replacement would be very similar. We assume that most use cases override our DataObject instead of implementing the IDataObject from scratch.
Should the consumption side APIs be T GetData<T>(..) or bool TryGetData<T>(… out T data).
No strong preference, an API that returns a Boolean seems to be more convenient for the common use patterns observed in GitHUB.
Naming for the configuration switch should indicate that it’s applicable to WPF as well when we share code with WPF.
After the code is merged in the WPF repo, System.Windows.Forms.Clipboard and System.Windows.Clipboard
will share the implementation. We want the configuration switch name to indicate that it's applicable to both. We could use the common namespace name portion:
System.Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization
Or
System.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization
Or no namespace name
ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization
Risks
• Users would have to implement the assembly loader and type resolver that works with TryGetData() to support types other than "T". If that resolver calls Type.GetType() they might lose control over assembly loading. Proposed APIs that do not accept the type resolver parameter, don't have this issue. When doing type matching, we rely on NrbfDecoder and type name matching API to be safe (threat model).
Sample user-provided resolver code:
internal static Type MyResolver(TypeName typeName)
{
Type[] allowedTypes =
[
typeof(MyClass),
typeof(MyClass1)
];
foreach (Type type in allowedTypes)
{
// Namespace-qualified type name.
if (typeName.FullName == type.FullName!)
{
return type;
}
}
// Do not call Type.GetType(typeName.AssemblyQualifiedName), throw for the unexpected types instead.
throw new NotSupportedException();
}
• When deserializing objects that have been JSON serialized, this carries the same risks as any JSON data going through System.Text.Json, so it is possible to misuse SetDataAsJson to do bad things during clipboard and drag/drop operation (System.Text.Json threat model). As with any data, users need to trust the JSON data they are trying to grab.
Risk mitigation
We are adding a new configuration switch that would block the fallback into BinaryFormatter use in Clipboard and DragDrop scenarios, the proposed APIs allow users to use JSON format instead.
Background and motivation
OLE clipboard supports standard exchange formats and additionally allows users to register custom formats for data exchange. Although standard exchange formats do not use binary format as they are defined by Windows, custom format serialization of objects in .NET has used the BinaryFormatter (in both WinForms and WPF). WinForms Clipboard falls back to the BinaryFormatter when consuming custom types stored as custom clipboard formats. We propose a safer set of Clipboard and DataObject APIs that restrict unbounded BinaryFormatter deserialization to types known at the compile time and provide an alternative serialization method, JSON, for user types.
API Proposal
We propose the same API changes in WinForms and WPF assemblies. The proposed APIs will support our updated Clipboard and Drag/Drop guidance which is:
BinaryFormatter
, use intrinsically handled types, which include primitive types, arrays or lists of primitives, exchange types from System.Drawing.Primitives.dll (PointF, RectangleF, Point, Rectangle, SizeF, Size, Color), and types commonly used in WinForms applications, such as System.Drawing.Bitmap. These types are handled for the user and continue to work without migration steps needed. We are open to adding more intrinsically handled types as needed.BinaryFormatter
with the proposed APIs, but might require custom JSON serialization. This is the recommended way for new application to use the Clipboard or Drag/Drop.byte[]
orstring
. Adjustment will need to be made on the consuming side to handle receiving a JSON formatted type.BinaryFormatter
deserialization to allowed types. This usage is acceptable during migration off BinaryFormatter and is not secure.Clipboard
Managed implementation of OLE's IDataObject definition and is being used in all OLE operations for WinForms/WPF.
DataObject
IDataObject
Control
DragDrop
ClipboardProxy
New configuration switch:
ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization
- controls whether BinaryFormatter is enabled as a fallback for Clipboard and Drag/drop scenarios. By default, it is false.API Usage
Clipboard API Examples
Before introduction of new APIs
public void SetGetClipboardData() { WeatherForecastPOCO weatherForecast = new() { Date = DateTime.Parse("2019-08-01"), TemperatureCelsius = 25, Summary = "Hot", };
pragma warning disable WFDEV005 // Type or member is obsolete
pragma warning restore WFDEV005
}
After introduction of new APIs
BinaryFormatter
.BinaryFormatter
deserialization by providing a set of allowed types.// Consumer side if (Clipboard.TryGetData("font", FontResolver, out Font data)) { // Do things with data }
private static Type FontResolver(TypeName typeName) { (string name, Type type)[] allowedTypes = [ (typeof(FontStyle).FullName!, typeof(FontStyle)), (typeof(FontFamily).FullName!, typeof(FontFamily)), (typeof(GraphicsUnit).FullName!, typeof(GraphicsUnit)), ];
}
Alternative Designs
Func<TypeName, Type>
with a resolver interface that can be reused in ResX and ActiveX scenarios and potentially in other Runtime scenarios, move the IResolver interface intoSystem.Runtime.Serialization
namespace. That would be a replacement for the resolverFunc<TypeName, Type>
.Should we make the
GetData
methods in the managedIDataObject
interface obsolete? These methods are implemented by the user, we don’t know if they are vulnerable or not. But they propagate a bad pattern by returning an unconstrained type (System.Object). There might be too many false positives if we obsolete them. The recommended way is for users to derive from the managed DataObject, not to implement the interface from scratch, and that scenario is covered.Should we make the
SetData
overloads obsolete? No, this scenario is not vulnerable, it propagates a bad pattern only when serializing more complex types. We will address this with an analyzer.Why take
T
forSetDataAsJson
/DoDragDropAsJson
APIs instead ofobject
? We need to save the original type of the data that is being passed in so that we can rehydrate the type when the user asks for it, meaning that we will need to rely on Object.GetType to get the type of the passed in data in SetDataAsJson / DoDragDropAsJson APIs. This is not trim friendly because it might use reflection.Should an optional parameter to
SetData
andDoDragDrop
APIs to indicate serializing with Json is desired instead of introducing a new API signature? This is an option, but it is again not trim friendly as we would need to rely on Object.GetType.Should we make the managed
IDataObject
interface obsolete? The shape of the managed interface matches that of the OLE interface, and the whole ecosystem depends on it, so any replacement would be very similar. We assume that most use cases override our DataObject instead of implementing the IDataObject from scratch.Should the consumption side APIs be
T GetData<T>(..)
orbool TryGetData<T>(… out T data)
. No strong preference, an API that returns a Boolean seems to be more convenient for the common use patterns observed in GitHUB.Naming for the configuration switch should indicate that it’s applicable to WPF as well when we share code with WPF. After the code is merged in the WPF repo,
System.Windows.Forms.Clipboard
andSystem.Windows.Clipboard
will share the implementation. We want the configuration switch name to indicate that it's applicable to both. We could use the common namespace name portion:System.Windows.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization
OrSystem.ClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization
Or no namespace nameClipboardDragDrop.EnableUnsafeBinaryFormatterSerialization
Risks
• Users would have to implement the assembly loader and type resolver that works with TryGetData() to support types other than "T". If that resolver calls Type.GetType() they might lose control over assembly loading. Proposed APIs that do not accept the type resolver parameter, don't have this issue. When doing type matching, we rely on
NrbfDecoder
and type name matching API to be safe (threat model). Sample user-provided resolver code:• When deserializing objects that have been JSON serialized, this carries the same risks as any JSON data going through System.Text.Json, so it is possible to misuse
SetDataAsJson
to do bad things during clipboard and drag/drop operation (System.Text.Json threat model). As with any data, users need to trust the JSON data they are trying to grab.Risk mitigation
We are adding a new configuration switch that would block the fallback into
BinaryFormatter
use in Clipboard and DragDrop scenarios, the proposed APIs allow users to use JSON format instead.Will this feature affect UI controls?
No