Skip to content

Commit a1930ad

Browse files
authored
Improve MotW handling and clarify the MSB3821 (#14015)
1 parent 96045c0 commit a1930ad

18 files changed

Lines changed: 185 additions & 69 deletions

documentation/wiki/ChangeWaves.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ Change wave checks around features will be removed in the release that accompani
2929

3030
## Current Rotation of Change Waves
3131

32+
### 18.9
33+
- [GenerateResource: typed ResX data/metadata entries in Mark-of-the-Web files are now treated as untrusted and blocked with MSB3821; unblock the file (or set MSBUILDDISABLEFEATURESFROMVERSION=18.9) to restore prior behavior. ResXFileRef entries are always blocked regardless of this wave.](https://github.com/dotnet/msbuild/pull/14015)
34+
3235
### 18.8
3336
- [RAR task: across multiple input properties, resolve relative paths against the project directory (not the process current directory)](https://github.com/dotnet/msbuild/pull/13319)
3437
- [Console, parallel console, and terminal loggers print the paths of log files written by registered loggers (e.g. file logger and binary logger) as part of the end-of-build summary.](https://github.com/dotnet/msbuild/pull/13577)

src/Framework/ChangeWaves.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ internal static class ChangeWaves
3636
internal static readonly Version Wave18_6 = new Version(18, 6);
3737
internal static readonly Version Wave18_7 = new Version(18, 7);
3838
internal static readonly Version Wave18_8 = new Version(18, 8);
39-
internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8];
39+
internal static readonly Version Wave18_9 = new Version(18, 9);
40+
internal static readonly Version[] AllWaves = [Wave17_10, Wave17_12, Wave17_14, Wave18_3, Wave18_4, Wave18_5, Wave18_6, Wave18_7, Wave18_8, Wave18_9];
4041

4142
/// <summary>
4243
/// Special value indicating that all features behind all Change Waves should be enabled.

src/Tasks.UnitTests/ResourceHandling/GenerateResource_Tests.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4620,4 +4620,80 @@ internal bool DoesPathNeedQuotes(string path)
46204620
return base.IsQuotingRequired(path);
46214621
}
46224622
}
4623+
4624+
/// <summary>
4625+
/// Tests for the content-only portion of the Mark-of-the-Web trust check
4626+
/// (<see cref="GenerateResource.ResourceFileContentsAreDangerous(System.IO.TextReader)"/>).
4627+
/// These exercise the danger classification independently of the platform-specific zone/COM check,
4628+
/// so they run on all target frameworks.
4629+
/// </summary>
4630+
public sealed class ResourceFileTrustTests
4631+
{
4632+
private static bool IsDangerous(string resxContents)
4633+
=> GenerateResource.ResourceFileContentsAreDangerous(new StringReader(resxContents));
4634+
4635+
[Fact]
4636+
public void PlainStringEntryIsNotDangerous()
4637+
{
4638+
string resx = @"<root>
4639+
<data name=""Greeting"" xml:space=""preserve""><value>Hello</value></data>
4640+
</root>";
4641+
4642+
IsDangerous(resx).ShouldBeFalse();
4643+
}
4644+
4645+
[Fact]
4646+
public void MimetypeEntryIsDangerous()
4647+
{
4648+
string resx = @"<root>
4649+
<data name=""Blob"" mimetype=""application/x-microsoft.net.object.bytearray.base64""><value>AAAA</value></data>
4650+
</root>";
4651+
4652+
IsDangerous(resx).ShouldBeTrue();
4653+
}
4654+
4655+
[Fact]
4656+
public void ResXFileRefEntryIsDangerousEvenWhenWave18_9Disabled()
4657+
{
4658+
string resx = @"<root>
4659+
<data name=""Logo"" type=""System.Resources.ResXFileRef, System.Windows.Forms""><value>logo.txt;System.String, mscorlib</value></data>
4660+
</root>";
4661+
4662+
// ResXFileRef detection is intentionally unconditional - it is not gated behind a ChangeWave -
4663+
// because it can trigger reading of an external linked file. Disable Wave18_9 to prove it still fires.
4664+
using TestEnvironment env = TestEnvironment.Create();
4665+
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_9.ToString());
4666+
ChangeWaves.ResetStateForTests();
4667+
4668+
IsDangerous(resx).ShouldBeTrue();
4669+
}
4670+
4671+
[Fact]
4672+
public void TypedEntryIsDangerousWhenWave18_9Enabled()
4673+
{
4674+
string resx = @"<root>
4675+
<data name=""Count"" type=""System.Int32, mscorlib""><value>42</value></data>
4676+
</root>";
4677+
4678+
using TestEnvironment env = TestEnvironment.Create();
4679+
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", string.Empty);
4680+
ChangeWaves.ResetStateForTests();
4681+
4682+
IsDangerous(resx).ShouldBeTrue();
4683+
}
4684+
4685+
[Fact]
4686+
public void TypedEntryIsNotDangerousWhenWave18_9Disabled()
4687+
{
4688+
string resx = @"<root>
4689+
<data name=""Count"" type=""System.Int32, mscorlib""><value>42</value></data>
4690+
</root>";
4691+
4692+
using TestEnvironment env = TestEnvironment.Create();
4693+
env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave18_9.ToString());
4694+
ChangeWaves.ResetStateForTests();
4695+
4696+
IsDangerous(resx).ShouldBeFalse();
4697+
}
4698+
}
46234699
}

src/Tasks/GenerateResource.cs

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,46 +1013,9 @@ private unsafe bool IsDangerous(String filename)
10131013
String.Equals(extension, ".resw", StringComparison.OrdinalIgnoreCase))
10141014
{
10151015
// XML files are only dangerous if there are unrecognized objects in them
1016-
dangerous = false;
1017-
10181016
using FileStream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
1019-
using XmlTextReader reader = new XmlTextReader(stream);
1020-
reader.DtdProcessing = DtdProcessing.Ignore;
1021-
reader.XmlResolver = null;
1022-
try
1023-
{
1024-
while (reader.Read())
1025-
{
1026-
if (reader.NodeType == XmlNodeType.Element)
1027-
{
1028-
string s = reader.LocalName;
1029-
1030-
// We only want to parse data nodes,
1031-
// the mimetype attribute gives the serializer
1032-
// that's requested.
1033-
if (reader.LocalName.Equals("data"))
1034-
{
1035-
if (reader["mimetype"] != null)
1036-
{
1037-
dangerous = true;
1038-
}
1039-
}
1040-
else if (reader.LocalName.Equals("metadata"))
1041-
{
1042-
if (reader["mimetype"] != null)
1043-
{
1044-
dangerous = true;
1045-
}
1046-
}
1047-
}
1048-
}
1049-
}
1050-
catch
1051-
{
1052-
// If we hit an error while parsing assume there's a dangerous type in this file.
1053-
dangerous = true;
1054-
}
1055-
stream.Close();
1017+
using StreamReader streamReader = new StreamReader(stream);
1018+
dangerous = ResourceFileContentsAreDangerous(streamReader);
10561019
}
10571020

10581021
return dangerous;
@@ -1090,9 +1053,82 @@ private void RecordItemsForDisconnectIfNecessary(IEnumerable<ITaskItem> items)
10901053
#endif // FEATURE_APPDOMAIN
10911054

10921055
/// <summary>
1093-
/// Computes the path to ResGen.exe for use in logging and for passing to the
1094-
/// nested ResGen task.
1056+
/// Scans the contents of a <c>.resx</c>/<c>.resw</c> file and determines whether it contains any entry
1057+
/// that requires the file to be trusted before it is processed by <see cref="GenerateResource"/>.
10951058
/// </summary>
1059+
/// <remarks>
1060+
/// This is the content-only portion of the Mark-of-the-Web trust check (see <c>IsDangerous</c>). It is kept
1061+
/// separate (and free of the zone/COM machinery) so it can be unit-tested on all target frameworks.
1062+
/// A resource is considered dangerous when a <c>data</c>/<c>metadata</c> element:
1063+
/// <list type="bullet">
1064+
/// <item>has a <c>mimetype</c> attribute (an arbitrary object will be deserialized), or</item>
1065+
/// <item>has a <c>type</c> attribute referencing <c>ResXFileRef</c> (an external linked file will be read), or</item>
1066+
/// <item>has any other <c>type</c> attribute - only when ChangeWave 18.9 is enabled - because resolving the
1067+
/// type eventually calls <c>Type.GetType</c>, which can probe for assemblies on disk.</item>
1068+
/// </list>
1069+
/// </remarks>
1070+
internal static bool ResourceFileContentsAreDangerous(TextReader textReader)
1071+
{
1072+
// XML files are only dangerous if there are unrecognized objects in them.
1073+
bool dangerous = false;
1074+
1075+
using XmlTextReader reader = new XmlTextReader(textReader);
1076+
reader.DtdProcessing = DtdProcessing.Ignore;
1077+
reader.XmlResolver = null;
1078+
try
1079+
{
1080+
while (reader.Read())
1081+
{
1082+
if (reader.NodeType == XmlNodeType.Element)
1083+
{
1084+
string localName = reader.LocalName;
1085+
1086+
// We only want to parse data nodes,
1087+
// the mimetype attribute gives the serializer
1088+
// that's requested.
1089+
if (localName.Equals("data") || localName.Equals("metadata"))
1090+
{
1091+
if (reader["mimetype"] != null)
1092+
{
1093+
dangerous = true;
1094+
}
1095+
else
1096+
{
1097+
// ResXFileRef can reference external files
1098+
string typeAttribute = reader["type"];
1099+
if (typeAttribute != null)
1100+
{
1101+
if (typeAttribute.IndexOf("ResXFileRef", StringComparison.Ordinal) >= 0)
1102+
{
1103+
dangerous = true;
1104+
}
1105+
else if (ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave18_9))
1106+
{
1107+
// Require any typed entry on a "Mark of the web" to be unblocked by user
1108+
dangerous = true;
1109+
}
1110+
}
1111+
}
1112+
}
1113+
}
1114+
1115+
// The result is already determined; no need to keep parsing the rest of the file.
1116+
if (dangerous)
1117+
{
1118+
break;
1119+
}
1120+
}
1121+
}
1122+
catch
1123+
{
1124+
// If we hit an error while parsing assume there's a dangerous type in this file.
1125+
dangerous = true;
1126+
}
1127+
1128+
return dangerous;
1129+
}
1130+
1131+
10961132
/// <returns>True if the path is found (or it doesn't matter because we're executing in memory), false otherwise</returns>
10971133
private bool ComputePathToResGen()
10981134
{

src/Tasks/Resources/Strings.resx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1160,7 +1160,7 @@
11601160
<comment>{StrBegin="MSB3820: "}</comment>
11611161
</data>
11621162
<data name="GenerateResource.MOTW">
1163-
<value>MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you want to process these files.</value>
1163+
<value>MSB3821: Couldn't process file {0} due to its being in the Internet or Restricted zone or having the mark of the web on the file. Remove the mark of the web if you trust these files and want to process them. Please follow MSBuild secure usage best practices - https://aka.ms/msbuild-security-documentation</value>
11641164
<comment>{StrBegin="MSB3821: "} "Internet zone", "Restricted zone", and "mark of the web" are Windows concepts that may have a specific translation.</comment>
11651165
</data>
11661166
<data name="GenerateResource.PreserializedResourcesRequiresExtensions">

src/Tasks/Resources/xlf/Strings.cs.xlf

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Tasks/Resources/xlf/Strings.de.xlf

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Tasks/Resources/xlf/Strings.es.xlf

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Tasks/Resources/xlf/Strings.fr.xlf

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Tasks/Resources/xlf/Strings.it.xlf

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)