DataSet 和 DataTable 安全性指引
本文適用於:
- .NET Framework (所有版本)
- .NET Core 和更新版本
- .NET 5 和更新版本
DataSet 和 DataTable 類型是舊版 .NET 元件,允許將資料集表示為受控物件。 這些元件是在 .NET Framework 1.0 中引進,作為原始 ADO.NET 基礎結構的一部分。 其目標是提供關聯式資料集的受控檢視,摘要出資料的基礎來源是 XML、SQL,還是其他技術。
如需 ADO.NET 的詳細資訊,包括更現代化的資料檢視範例,請參閱 ADO.NET 文件。
從 XML 還原序列化 DataSet 或 DataTable 時的預設限制
在所有支援的 .NET Framework、.NET Core 和 .NET 版本上,DataSet
和 DataTable
會對還原序列化資料中可能存在的物件類型設定下列限制。 根據預設,此清單會限制為:
- 基本類型和基本對等項:
bool
、char
sbyte
byte
short
ushort
int
uint
long
ulong
float
double
decimal
DateTime
DateTimeOffset
TimeSpan
string
Guid
SqlBinary
SqlBoolean
SqlByte
SqlBytes
SqlChars
SqlDateTime
SqlDecimal
SqlDouble
SqlGuid
SqlInt16
SqlInt32
SqlInt64
SqlMoney
SqlSingle
和SqlString
。 - 常用的非基本類型:
Type
、Uri
和BigInteger
。 - 常用的 System.Drawing 類型:
Color
、Point
、PointF
、Rectangle
、RectangleF
、Size
和SizeF
。 Enum
類型。- 上述類型的陣列和清單。
如果傳入 XML 資料包含其類型不在這份清單中的物件:
擲回例外狀況,隨附下列訊息和堆疊追蹤。 錯誤訊息:System.InvalidOperationException : 這裡不允許類型 '<Type Name>, Version=<n.n.n.n>, Culture=<culture>, PublicKeyToken=<token value>'。 堆疊追蹤:at System.Data.TypeLimiter.EnsureTypeIsAllowed(Type type, TypeLimiter capturedLimiter) at System.Data.DataColumn.UpdateColumnType(Type type, StorageType typeCode) at System.Data.DataColumn.set_DataType(Type value)
還原序列化作業失敗。
將 XML 載入至現有的 DataSet
或 DataTable
執行個體時,也會考慮現有的資料行定義。 如果資料表已包含自訂類型的資料行定義,該類型會在 XML 還原序列化作業期間暫時新增至允許清單。
注意
一旦您將資料行新增至 DataTable
, ReadXml
就不會從 XML 讀取結構描述,而且如果結構描述不符,其也不會在記錄中讀取,因此您必須自行新增所有資料行,才能使用此方法。
XmlReader xmlReader = GetXmlReader();
// Assume the XML blob contains data for type MyCustomClass.
// The following call to ReadXml fails because MyCustomClass isn't in the allowed types list.
DataTable table = new DataTable("MyDataTable");
table.ReadXml(xmlReader);
// However, the following call to ReadXml succeeds, since the DataTable instance
// already defines a column of type MyCustomClass.
DataTable table = new DataTable("MyDataTable");
table.Columns.Add("MyColumn", typeof(MyCustomClass));
table.ReadXml(xmlReader); // this call will succeed
使用 XmlSerializer
還原序列化 DataSet
或 DataTable
的執行個體時,物件類型限制也適用。 不過,使用 BinaryFormatter
還原序列化 DataSet
或 DataTable
的執行個體時,這些限制可能不適用。
使用 DataAdapter.Fill
時 (例如,直接從資料庫填入 DataTable
執行個體,而不使用 XML 還原序列化 API 時),物件類型限制不適用。
擴充允許的類型清單
除了上面所列的內建類型之外,應用程式還可以擴充允許的類型清單,以包含自訂類型。 如果擴充允許的類型清單,變更會影響應用程式內的所有 DataSet
和 DataTable
執行個體。 無法從內建的允許類型清單中移除類型。
透過組態進行擴充 (.NET Framework 4.0 和更新版本)
App.config 可以用來擴充允許的類型清單。 若要擴充允許的類型清單:
- 使用
<configSections>
元素,將參考新增至 System.Data 組態區段。 - 使用
<system.data.dataset.serialization>
/<allowedTypes>
來指定其他類型。
每個 <add>
元素必須使用其組件限定類型名稱,僅指定一個類型。 若要將其他類型新增至允許的類型清單,請使用多個 <add>
元素。
下列範例說明藉由新增自訂類型 Fabrikam.CustomType
來擴充允許的類型清單。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="system.data.dataset.serialization" type="System.Data.SerializationSettingsSectionGroup, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<section name="allowedTypes" type="System.Data.AllowedTypesSectionHandler, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</sectionGroup>
</configSections>
<system.data.dataset.serialization>
<allowedTypes>
<!-- <add type="assembly qualified type name" /> -->
<add type="Fabrikam.CustomType, Fabrikam, Version=1.0.0.0, Culture=neutral, PublicKeyToken=2b3831f2f2b744f7" />
<!-- additional <add /> elements as needed -->
</allowedTypes>
</system.data.dataset.serialization>
</configuration>
若要擷取類型的組件限定名稱,請使用 Type.AssemblyQualifiedName 屬性,如下列程式碼所示範。
string assemblyQualifiedName = typeof(Fabrikam.CustomType).AssemblyQualifiedName;
透過組態進行擴充 (.NET Framework 4.0 和 2.0 - 3.5)
如果您的應用程式以 .NET Framework 2.0 或 3.5 為目標,您仍然可以使用上述 App.config 機制來擴充允許的類型清單。 不過,您的 <configSections>
元素看起來會稍有不同,如下列程式碼所示:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<!-- The below <sectionGroup> and <section> are specific to .NET Framework 2.0 and 3.5. -->
<sectionGroup name="system.data.dataset.serialization" type="System.Data.SerializationSettingsSectionGroup, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<section name="allowedTypes" type="System.Data.AllowedTypesSectionHandler, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"/>
</sectionGroup>
</configSections>
<system.data.dataset.serialization>
<allowedTypes>
<!-- <add /> elements, as demonstrated in the .NET Framework 4.0 - 4.8 sample code above. -->
</allowedTypes>
</system.data.dataset.serialization>
</configuration>
以程序設計方式擴充 (.NET Framework、.NET Core、.NET 5+)
允許的類型清單也可以使用 AppDomain.SetData 搭配已知的機碼 System.Data.DataSetDefaultAllowedTypes,以程式設計方式擴充,如下列程式碼所示。
Type[] extraAllowedTypes = new Type[]
{
typeof(Fabrikam.CustomType),
typeof(Contoso.AdditionalCustomType)
};
AppDomain.CurrentDomain.SetData("System.Data.DataSetDefaultAllowedTypes", extraAllowedTypes);
如果使用擴充機制,與索引鍵 System.Data.DataSetDefaultAllowedTypes 相關聯的值,其類型必須是 Type[]
。
在 .NET Framework 上,可以同時使用 App.config 和 AppDomain.SetData
來擴充允許的類型清單。 在此情況下,如果物件的類型存在於任一清單中,則 DataSet
DataTable
將允許該物件進行還原序列化,作為資料的一部分。
以稽核模式執行應用程式 (.NET Framework)
在 .NET Framework 中,DataSet
和 DataTable
提供稽核模式功能。 啟用稽核模式時,DataSet
和DataTable
會將連入物件的類型與允許的類型清單進行比較。 不過,如果看到不允許其類型的物件,則不會擲回例外狀況。 相反地,DataSet
和 DataTable
會通知任何附加的 TraceListener
執行個體,存在可疑類型,同時允許 TraceListener
記錄這項資訊。 系統不會擲回任何例外狀況,且還原序列化作業會繼續。
警告
以「稽核模式」執行應用程式應該只是用於測試的暫時措施。 啟用稽核模式時,DataSet
和 DataTable
不會強制執行類型限制,這可能會在您的應用程式內引入安全性漏洞。 如需詳細資訊,請參閱標題為移除所有類型限制和與不受信任輸入相關的安全章節。
您可以透過 App.config來啟用稽核模式:
- 如需要為
<configSections>
元素放置適當值的相關資訊,請參閱本文件中的透過組態進行擴充一節。 - 使用
<allowedTypes auditOnly="true">
來啟用稽核模式,如下列標記所示。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<!-- See the section of this document titled "Extending through configuration" for the appropriate
<sectionGroup> and <section> elements to put here, depending on whether you're running .NET
Framework 2.0 - 3.5 or 4.0 - 4.8. -->
</configSections>
<system.data.dataset.serialization>
<allowedTypes auditOnly="true"> <!-- setting auditOnly="true" enables audit mode -->
<!-- Optional <add /> elements as needed. -->
</allowedTypes>
</system.data.dataset.serialization>
</configuration>
一旦啟用了稽核模式,就可以使用 App.config,將慣用的 TraceListener
連線到 DataSet
內建 TraceSource.
。連線追蹤的名稱為 System.Data.DataSet。 下列範例示範如何將追蹤事件寫入主控台,以及寫入磁碟上的記錄檔。
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<system.diagnostics>
<sources>
<source name="System.Data.DataSet"
switchType="System.Diagnostics.SourceSwitch"
switchValue="Warning">
<listeners>
<!-- write to the console -->
<add name="console"
type="System.Diagnostics.ConsoleTraceListener" />
<!-- *and* write to a log file on disk -->
<add name="filelog"
type="System.Diagnostics.TextWriterTraceListener"
initializeData="c:\logs\mylog.txt" />
</listeners>
</source>
</sources>
</system.diagnostics>
</configuration>
如需 TraceSource
和 TraceListener
的詳細資訊,請參閱作法:搭配追蹤接聽程式使用 TraceSource 和篩選條件一文。
注意
以稽核模式執行應用程式不適用於 .NET Core 或 .NET 5 和更新版本。
移除所有類型限制
如果應用程式必須從 DataSet
和 DataTable
移除所有類型限制:
- 有數個選項可隱藏類型限制。
- 可用的選項取決於應用程式的目標架構。
警告
移除所有類型限制可能會在應用程式內引入安全性漏洞。 使用此機制時,請確定應用程式不會 使用 DataSet
或 DataTable
讀取不受信任的輸入。 如需詳細資訊,請參閱 CVE-2020-1147 和下一節,標題為與不受信任輸入相關的安全。
透過 AppContext 組態 (.NET Framework 4.6 和更新版本、.NET Core 2.1 和更新版本、.NET 5 和更新版本)
設定為 true
時,AppContext
參數 (Switch.System.Data.AllowArbitraryDataSetTypeInstantiation
) 會從 DataSet
和 DataTable
移除所有類型限制。
在 .NET Framework 中,可以透過 App.config 啟用此參數,如下列組態所示:
<configuration>
<runtime>
<!-- Warning: setting the following switch can introduce a security problem. -->
<AppContextSwitchOverrides value="Switch.System.Data.AllowArbitraryDataSetTypeInstantiation=true" />
</runtime>
</configuration>
在 ASP.NET 中,<AppContextSwitchOverrides>
元素無法使用。 相反地,可以透過 Web.config 啟用參數,如下列組態所示:
<configuration>
<appSettings>
<!-- Warning: setting the following switch can introduce a security problem. -->
<add key="AppContext.SetSwitch:Switch.System.Data.AllowArbitraryDataSetTypeInstantiation" value="true" />
</appSettings>
</configuration>
如需詳細資訊,請參閱 <AppContextSwitchOverrides> 元素。
在 .NET Core、.NET 5 和 ASP.NET Core 中,此設定是由 runtimeconfig.json 控制,如下列 JSON 所示:
{
"runtimeOptions": {
"configProperties": {
"Switch.System.Data.AllowArbitraryDataSetTypeInstantiation": true
}
}
}
如需詳細資訊,請參閱「.NET Core 執行階段組態設定」。
AllowArbitraryDataSetTypeInstantiation
也可以透過 AppContext.SetSwitch 以程式設計方式設定,而不是使用組態檔,如下列程式碼所示:
// Warning: setting the following switch can introduce a security problem.
AppContext.SetSwitch("Switch.System.Data.AllowArbitraryDataSetTypeInstantiation", true);
如果您選擇上述的程式設計方法,應該會在應用程式啟動時提早呼叫 AppContext.SetSwitch
。
透過全電腦登錄 (.NET Framework 2.0 - 4.x)
如果 AppContext
無法使用,則可以使用 Windows 登錄停用類型限制檢查:
- 管理員必須設定登錄。
- 使用登錄是全電腦變更,而且會影響電腦上執行的「所有」應用程式。
類型 | 值 |
---|---|
登錄機碼 | HKLM\SOFTWARE\Microsoft\.NETFramework\AppContext |
值名稱 | Switch.System.Data.AllowArbitraryDataSetTypeInstantiation |
值類型 | REG_SZ |
值資料 | true |
在 64 位元作業系統上,我需要同時針對 64 位元金鑰 (如上所示) 和 32 位元金鑰新增此值。 32 位元金鑰位於 HKLM\SOFTWARE\WOW6432Node\Microsoft\.NETFramework\AppContext
。
如需使用登錄來設定 AppContext
的詳細資訊,請參閱「程式庫取用者的 AppContext」。
與不受信任輸入相關的安全
雖然 DataSet
和 DataTable
確實會對在還原序列化 XML 承載時允許存在的類型強加預設限制,但 DataSet
和 DataTable
在填入不受信任的輸入時通常不安全。以下是非詳盡清單,列出 DataSet
或 DataTable
執行個體可能讀取不受信任輸入的方式。
DataAdapter
會參考資料庫,而且DataAdapter.Fill
方法用來將資料庫查詢的內容填入DataSet
。DataSet.ReadXml
或DataTable.ReadXml
方法用來讀取包含資料行和資料列資訊的 XML 檔案。DataSet
或DataTable
會序列化為 ASP.NET (SOAP) Web 服務或 WCF 端點的一部分。XmlSerializer
這類的序列化程式用來從 XML 資料流還原序列化DataSet
或DataTable
執行個體。JsonConvert
這類的序列化程式用來從 JSON 資料流還原序列化DataSet
或DataTable
執行個體。JsonConvert
是著名協力廠商 Newtonsoft.Json 程式庫中的方法。BinaryFormatter
這類的序列化程式用來從原始位元組資料流還原序列化DataSet
或DataTable
執行個體。
本文件討論上述案例的安全考量。
使用 DataAdapter.Fill
從不受信任的資料來源填入 DataSet
您可以使用 DataAdapter.Fill
方法從 DataAdapter
填入 DataSet
執行個體,如下列範例所示。
// Assumes that connection is a valid SqlConnection object.
string queryString =
"SELECT CustomerID, CompanyName FROM dbo.Customers";
SqlDataAdapter adapter = new SqlDataAdapter(queryString, connection);
DataSet customers = new DataSet();
adapter.Fill(customers, "Customers");
(上述程式碼範例是從 DataAdapter 填入 DataSet 中所找到較大範例的一部分。)
大部分的應用程式都可以簡化並假設其資料庫層受到信任。 不過,如果您習慣對應用程式使用威脅模型化 ,則您的威脅模型可能會認為應用程式 (用戶端) 和資料庫層 (伺服器) 之間會有信任界限。 在用戶端與伺服器之間使用相互驗證或 AAD 驗證,是協助解決與此相關聯風險的一種方式。 本節的其餘部分會討論用戶端連線到不受信任伺服器的可能結果。
指向不受信任資料來源中 DataAdapter
的結果取決於 DataAdapter
本身的實作。
SqlDataAdapter
針對內建類型 SqlDataAdapter,參考不受信任的資料來源可能會導致拒絕服務 (DoS) 攻擊。 DoS 攻擊可能會導致應用程式沒有回應或當機。 如果攻擊者可以在應用程式旁邊植入 DLL,他們也能夠實現本機程式碼執行。
其他 DataAdapter 類型
協力廠商 DataAdapter
實作必須自行評估他們面對不受信任輸入時所提供的安全性保證。 .NET 無法針對這些實作做出任何安全保證。
DataSet.ReadXml 和 DataTable.ReadXml
與不受信任的輸入搭配使用時,DataSet.ReadXml
和 DataTable.ReadXml
方法並不安全。 強烈建議取用者改為考慮使用本文件稍後所述的其中一個替代方案。
DataSet.ReadXml
和 DataTable.ReadXml
的實作最初是在序列化弱點為眾所周知威脅類別之前所建立。 因此,程式碼不會遵循目前的安全性最佳做法。 這些 API 可以用作攻擊者針對 Web 應用程式執行 DoS 攻擊的媒介。 這些攻擊可能會使 Web 服務沒有回應,或導致非預期的處理序終止。 此架構不會為這些攻擊類別提供風險降低措施,而且 .NET 會將此行為視為「設計」所致。
.NET 已發行安全性更新,以緩解 DataSet.ReadXml
和 DataTable.ReadXml
中的資訊洩漏或遠端程式碼執行等一些問題。 .NET 安全性更新可能無法針對這些威脅類別提供完整保護。 取用者應該評估其個別案例,並考慮到他們可能暴露在這些風險中。
取用者應該注意,這些 API 的安全性更新在某些情況下可能會影響應用程式相容性。 此外,可能會在這些 API 中探索到新型弱點,但實際上 .NET 無法為這些弱點發佈安全性更新。
建議這些 API 的取用者:
- 考慮使用本文件稍後所述的其中一個替代方案。
- 在其應用程式上執行個別風險評估。
取用者必須負責判斷是否要使用這些 API。 取用者應該評估使用這些 API 可能伴隨的任何安全性、技術和法律風險,包括法規需求。
透過 ASP.NET Web 服務或 WCF 的 DataSet 和 DataTable
可以接受 ASP.NET (SOAP) Web 服務中的 DataSet
或 DataTable
執行個體,如下列程式碼所示:
using System.Data;
using System.Web.Services;
[WebService(Namespace = "http://contoso.com/")]
public class MyService : WebService
{
[WebMethod]
public string MyWebMethod(DataTable dataTable)
{
/* Web method implementation. */
}
}
此情況的變體不是直接接受 DataSet
或 DataTable
作為參數,而是改為接受其作為整體 SOAP 序列化物件圖形的一部分,如下列程式碼所示:
using System.Data;
using System.Web.Services;
[WebService(Namespace = "http://contoso.com/")]
public class MyService : WebService
{
[WebMethod]
public string MyWebMethod(MyClass data)
{
/* Web method implementation. */
}
}
public class MyClass
{
// Property of type DataTable, automatically serialized and
// deserialized as part of the overall MyClass payload.
public DataTable MyDataTable { get; set; }
}
或者,使用 WCF 而不是 ASP.NET Web 服務:
using System.Data;
using System.ServiceModel;
[ServiceContract(Namespace = "http://contoso.com/")]
public interface IMyContract
{
[OperationContract]
string MyMethod(DataTable dataTable);
[OperationContract]
string MyOtherMethod(MyClass data);
}
public class MyClass
{
// Property of type DataTable, automatically serialized and
// deserialized as part of the overall MyClass payload.
public DataTable MyDataTable { get; set; }
}
在所有這些情況下,威脅模型和安全性保證與 DataSet.ReadXml 和 DataTable.ReadXml 區段相同。
透過 XmlSerializer 還原序列化 DataSet 或 DataTable
開發人員可以使用 XmlSerializer
還原序列化 DataSet
和 DataTable
執行個體,如下列程式碼所示:
using System.Data;
using System.IO;
using System.Xml.Serialization;
public DataSet PerformDeserialization1(Stream stream) {
XmlSerializer serializer = new XmlSerializer(typeof(DataSet));
return (DataSet)serializer.Deserialize(stream);
}
public MyClass PerformDeserialization2(Stream stream) {
XmlSerializer serializer = new XmlSerializer(typeof(MyClass));
return (MyClass)serializer.Deserialize(stream);
}
public class MyClass
{
// Property of type DataTable, automatically serialized and
// deserialized as part of the overall MyClass payload.
public DataTable MyDataTable { get; set; }
}
在這些情況下,威脅模型和安全性保證與 DataSet.ReadXml 和 DataTable.ReadXml 區段相同
透過 JsonConvert 還原序列化 DataSet 或 DataTable
著名協力廠商 Newtonsoft 程式庫 Json.NET 可以用來還原序列化 DataSet
和 DataTable
執行個體,如下列程式碼所示:
using System.Data;
using Newtonsoft.Json;
public DataSet PerformDeserialization1(string json) {
return JsonConvert.DeserializeObject<DataSet>(data);
}
public MyClass PerformDeserialization2(string json) {
return JsonConvert.DeserializeObject<MyClass>(data);
}
public class MyClass
{
// Property of type DataTable, automatically serialized and
// deserialized as part of the overall MyClass payload.
public DataTable MyDataTable { get; set; }
}
以這種方式從不受信任的 JSON Blob 還原序列化 DataSet
或 DataTable
並不安全。 這種模式很容易遭受拒絕服務的攻擊。 這類攻擊可能會損毀應用程式,或使其沒有回應。
注意
Microsoft 不保證或支援協力廠商程式庫的實作,例如 Newtonsoft.Json。 這項資訊是針對完整性而提供,且在撰寫本文時正確無誤。
透過 BinaryFormatter 還原序列化 DataSet 或 DataTable
您絕不能使用 BinaryFormatter
、NetDataContractSerializer
、SoapFormatter
或相關的不安全格式器,從不受信任的承載還原序列化 DataSet
或 DataTable
執行個體:
- 這很容易遭受完整的遠端程式碼執行攻擊。
- 使用自訂
SerializationBinder
並不足以防止這類攻擊。
安全取代
針對下列情況的應用程式:
- 透過 .asmx SOAP 端點或 WCF 端點接受
DataSet
或DataTable
。 - 將不受信任的資料還原序列化為
DataSet
或DataTable
的執行個體。
考慮取代物件模型以使用 Entity Framework。 Entity Framework:
- 這是一種豐富的現式物件導向架構,可以代表關聯式資料。
- 帶來資料庫提供者的各種生態系統,讓您能夠輕鬆地透過 Entity Framework 物件模型來投影資料庫查詢。
- 從不受信任的來源還原序列化資料時提供內建保護。
對於使用 .aspx
SOAP 端點的應用程式,請考慮將這些端點變更為使用 WCF。 WCF 是取代 .asmx
Web 服務且功能更完整的項目。 WCF 端點可以透過 SOAP 公開,以與現有的呼叫端相容。
程式碼分析器
在原始程式碼編譯時執行的程式碼分析器安全性規則,有助於在 C# 和 Visual Basic 程式碼中尋找與這個安全性問題相關的弱點。 Microsoft.CodeAnalysis.FxCopAnalyzers 是散發在 nuget.org 上的 NuGet 程式碼分析器套件。
如需程式碼分析器的概觀,請參閱原始程式碼分析器概觀。
啟用下列 Microsoft.CodeAnalysis.FxCopAnalyzers 規則:
- CA2350:使用 DataTable.ReadXml() 時請勿附上不受信任的資料
- CA2351:使用 DataSet.ReadXml() 時請勿附上不受信任的資料
- CA2352:可序列化類型中的不安全 DataSet 或 DataTable 可能容易受到遠端程式碼執行攻擊
- CA2353:可序列化類型中的不安全 DataSet 或 DataTable
- CA2354:還原序列化物件圖中的不安全 DataSet 或 DataTable 可能容易受到遠端程式碼執行攻擊
- CA2355:在可還原序列化的物件圖形中,找到不安全的 DataSet 或 DataTable 類型
- CA2356:Web 可還原序列化物件圖形中不安全的 DataSet 或 DataTable 類型
- CA2361:確認不會搭配不受信任的資料使用包含 DataSet.ReadXml() 的自動產生類別
- CA2362:自動產生之可序列化型別中不安全的 DataSet 或 DataTable,容易受到遠端程式碼執行攻擊
如需設定規則的詳細資訊,請參閱使用程式碼分析器。
新的安全性規則可在下列 NuGet 套件中取得:
- Microsoft.CodeAnalysis.FxCopAnalyzers 3.3.0:適用於 Visual Studio 2019 16.3 版或更新版本
- Microsoft.CodeAnalysis.FxCopAnalyzers 2.9.11:適用於 Visual Studio 2017 15.9 版或更新版本