diff --git a/ECoreNetto.Tests/Resource/XxeHardeningTestFixture.cs b/ECoreNetto.Tests/Resource/XxeHardeningTestFixture.cs new file mode 100644 index 0000000..97ac192 --- /dev/null +++ b/ECoreNetto.Tests/Resource/XxeHardeningTestFixture.cs @@ -0,0 +1,92 @@ +// ------------------------------------------------------------------------------------------------- +// +// +// Copyright 2017-2025 Starion Group S.A. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +// ------------------------------------------------------------------------------------------------ + +namespace ECoreNetto.Tests.Resource +{ + using System.IO; + using System.Xml; + + using ECoreNetto.Resource; + + using NUnit.Framework; + + /// + /// Suite of tests that verify the is hardened against + /// XML External Entity (XXE) attacks (see issue #29). + /// + [TestFixture] + public class XxeHardeningTestFixture + { + private ResourceSet resourceSet = null!; + + [SetUp] + public void SetUp() + { + this.resourceSet = new ResourceSet(); + } + + [Test] + public void Verify_that_a_document_with_an_external_entity_is_rejected() + { + // a DOCTYPE declaring an external SYSTEM entity - the classic XXE payload + const string xxe = + "\r\n" + + " ]>\r\n" + + "&xxe;"; + + var resource = this.CreateResourceForContent("xxe-attack.ecore", xxe); + + // DtdProcessing.Prohibit rejects the DOCTYPE before the entity is ever resolved + Assert.That(() => resource.Load(null), Throws.InstanceOf()); + } + + [Test] + public void Verify_that_an_entity_expansion_document_is_rejected() + { + // a "billion laughs" style nested-entity document; also blocked by prohibiting DTDs + const string billionLaughs = + "\r\n" + + "\r\n" + + " \r\n" + + " \r\n" + + "]>\r\n" + + "&lol3;"; + + var resource = this.CreateResourceForContent("billion-laughs.ecore", billionLaughs); + + Assert.That(() => resource.Load(null), Throws.InstanceOf()); + } + + /// + /// Writes the provided to a file in the test directory and + /// creates a for it. + /// + private Resource CreateResourceForContent(string fileName, string content) + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, fileName); + File.WriteAllText(path, content); + + var uri = new System.Uri(Path.GetFullPath(path)); + + return this.resourceSet.CreateResource(uri); + } + } +} diff --git a/ECoreNetto/ECoreParser.cs b/ECoreNetto/ECoreParser.cs index 111c6c1..4dbb6b0 100644 --- a/ECoreNetto/ECoreParser.cs +++ b/ECoreNetto/ECoreParser.cs @@ -81,13 +81,19 @@ internal EPackage ParseXml() var sw = Stopwatch.StartNew(); - var settings = new XmlReaderSettings(); + // harden against XXE: prohibit DTD processing and disable external entity resolution + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null + }; + var fileInfo = new FileInfo(this.resource.URI.AbsolutePath.Replace("%20", " ")); var fullPath = Path.GetFullPath(fileInfo.FullName); // now read the actual model file var xmlReader = XmlReader.Create(fullPath, settings); - var xmlDocument = new XmlDocument(); + var xmlDocument = new XmlDocument { XmlResolver = null }; xmlDocument.Load(xmlReader); var package = new EPackage(this.resource, this.loggerFactory); package.ReadXml(xmlDocument.DocumentElement);