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);