CVE-2020-26247 (published 2020-12-30T19:15Z)

Nokogiri is a Rubygem providing HTML, XML, SAX, and Reader parsers with XPath and CSS selector support. In Nokogiri before version 1.11.0.rc4 there is an XXE vulnerability. XML Schemas parsed by Nokogiri::XML::Schema are trusted by default, allowing external resources to be accessed over the network, potentially enabling XXE or SSRF attacks. This behavior is counter to the security policy followed by Nokogiri maintainers, which is to treat all input as untrusted by default whenever possible. This is fixed in Nokogiri version 1.11.0.rc4.

Severity: MEDIUM
CVSS v2 base score: 4.0 (AV:N/AC:L/Au:S/C:P/I:N/A:N)
CVSS v3 base score: 4.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N)

Weakness information:
cwe_id: CWE-611
cwe_name: Improper Restriction of XML External Entity Reference
description: The product processes an XML document that can contain XML entities with URIs that resolve to documents outside of the intended sphere of control, causing the product to embed incorrect documents into its output.
extended_description: 
            XML documents optionally contain a Document Type Definition (DTD), which, among other features, enables the definition of XML entities. It is possible to define an entity by providing a substitution string in the form of a URI. The XML parser can access the contents of this URI and embed these contents back into the XML document for further processing.
            By submitting an XML file that defines an external entity with a file:// URI, an attacker can cause the processing application to read the contents of a local file. For example, a URI such as "file:///c:/winnt/win.ini" designates (in Windows) the file C:\Winnt\win.ini, or file:///etc/passwd designates the password file in Unix-based systems. Using URIs with other schemes such as http://, the attacker can force the application to make outgoing requests to servers that the attacker cannot reach directly, which can be used to bypass firewall restrictions or hide the source of attacks such as port scanning.
            Once the content of the URI is read, it is fed back into the application that is processing the XML. This application may echo back the data (e.g. in an error message), thereby exposing the file contents.
         
         

Fix information:
repo_url: https://github.com/sparklemotion/nokogiri
commit_details: repo_url: https://github.com/sparklemotion/nokogiri
author: Mike Dalessio
author_date: 2020-11-23 00:47:02-05:00
msg: feat: XML::Schema and RelaxNG creation accept optional ParseOptions

I'm trying out a new pattern, which is that the parsed object carries
around the ParseOptions it was created with, which should make some
testing a bit easier.

I'm also not implementing the "config block" pattern in use for
Documents, because I think the UX is weird and I'm hoping to change
everything to use kwargs in a 2.0 release, anyway.
repository: repo_url: https://github.com/sparklemotion/nokogiri
repo_name: sparklemotion/nokogiri
description: Nokogiri (鋸) makes it easy and painless to work with XML and HTML from Ruby.
repo_language: C
owner: sparklemotion
file_changes: filename: XmlRelaxng.java
change_type: ModificationType.MODIFY
diff: @@ -56,6 +56,7 @@ import org.jruby.Ruby;
 import org.jruby.RubyClass;
 import org.jruby.anno.JRubyClass;
 import org.jruby.runtime.ThreadContext;
+import org.jruby.runtime.builtin.IRubyObject;
 import org.w3c.dom.Document;
 import org.xml.sax.ErrorHandler;
 import org.xml.sax.SAXException;
@@ -78,11 +79,17 @@ public class XmlRelaxng extends XmlSchema {
         this.verifier = verifier;
     }
     
-    static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source) {
+    static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source, IRubyObject parseOptions) {
         Ruby runtime = context.getRuntime();
         XmlRelaxng xmlRelaxng = (XmlRelaxng) NokogiriService.XML_RELAXNG_ALLOCATOR.allocate(runtime, klazz);
+
+        if (parseOptions == null) {
+            parseOptions = defaultParseOptions(context.getRuntime());
+        }
+
         xmlRelaxng.setInstanceVariable("@errors", runtime.newEmptyArray());
-        
+        xmlRelaxng.setInstanceVariable("@parse_options", parseOptions);
+
         try {
             Schema schema = xmlRelaxng.getSchema(source, context);
             xmlRelaxng.setVerifier(schema.newVerifier());

programming_language: Java
filename: XmlSchema.java
change_type: ModificationType.MODIFY
diff: @@ -106,10 +106,16 @@ public class XmlSchema extends RubyObject {
         this.validator = validator;
     }
 
-    static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source) {
+    static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source, IRubyObject parseOptions) {
         Ruby runtime = context.getRuntime();
         XmlSchema xmlSchema = (XmlSchema) NokogiriService.XML_SCHEMA_ALLOCATOR.allocate(runtime, klazz);
+
+        if (parseOptions == null) {
+            parseOptions = defaultParseOptions(context.getRuntime());
+        }
+
         xmlSchema.setInstanceVariable("@errors", runtime.newEmptyArray());
+        xmlSchema.setInstanceVariable("@parse_options", parseOptions);
 
         try {
             SchemaErrorHandler error_handler = new SchemaErrorHandler(context.getRuntime(), (RubyArray)xmlSchema.getInstanceVariable("@errors"));
@@ -121,14 +127,24 @@ public class XmlSchema extends RubyObject {
         }
     }
 
+    protected static IRubyObject defaultParseOptions(Ruby runtime) {
+        return ((RubyClass)runtime.getClassFromPath("Nokogiri::XML::ParseOptions")).getConstant("DEFAULT_SCHEMA");
+    }
+
     /*
      * call-seq:
      *  from_document(doc)
      *
      * Create a new Schema from the Nokogiri::XML::Document +doc+
      */
-    @JRubyMethod(meta=true)
-    public static IRubyObject from_document(ThreadContext context, IRubyObject klazz, IRubyObject document) {
+    @JRubyMethod(meta=true, required=1, optional=1)
+    public static IRubyObject from_document(ThreadContext context, IRubyObject klazz, IRubyObject[] args) {
+        IRubyObject document = args[0];
+        IRubyObject parseOptions = null;
+        if (args.length > 1) {
+            parseOptions = args[1];
+        }
+
         XmlDocument doc = ((XmlDocument) ((XmlNode) document).document(context));
 
         RubyArray errors = (RubyArray) doc.getInstanceVariable("@errors");
@@ -144,25 +160,30 @@ public class XmlSchema extends RubyObject {
             source.setSystemId(uri.convertToString().asJavaString());
         }
 
-        return getSchema(context, (RubyClass)klazz, source);
+        return getSchema(context, (RubyClass)klazz, source, parseOptions);
     }
 
-    private static IRubyObject getSchema(ThreadContext context, RubyClass klazz, Source source) {
+    @JRubyMethod(meta=true, required=1, optional=1)
+    public static IRubyObject read_memory(ThreadContext context, IRubyObject klazz, IRubyObject[] args) {
+        IRubyObject content = args[0];
+        IRubyObject parseOptions = null;
+        if (args.length > 1) {
+            parseOptions = args[1];
+        }
+        String data = content.convertToString().asJavaString();
+        return getSchema(context, (RubyClass) klazz, new StreamSource(new StringReader(data)), parseOptions);
+    }
+
+    private static IRubyObject getSchema(ThreadContext context, RubyClass klazz, Source source, IRubyObject parseOptions) {
         String moduleName = klazz.getName();
         if ("Nokogiri::XML::Schema".equals(moduleName)) {
-            return XmlSchema.createSchemaInstance(context, klazz, source);
+            return XmlSchema.createSchemaInstance(context, klazz, source, parseOptions);
         } else if ("Nokogiri::XML::RelaxNG".equals(moduleName)) {
-            return XmlRelaxng.createSchemaInstance(context, klazz, source);
+            return XmlRelaxng.createSchemaInstance(context, klazz, source, parseOptions);
         }
         return context.getRuntime().getNil();
     }
 
-    @JRubyMethod(meta=true)
-    public static IRubyObject read_memory(ThreadContext context, IRubyObject klazz, IRubyObject content) {
-        String data = content.convertToString().asJavaString();
-        return getSchema(context, (RubyClass) klazz, new StreamSource(new StringReader(data)));
-    }
-
     @JRubyMethod(visibility=Visibility.PRIVATE)
     public IRubyObject validate_document(ThreadContext context, IRubyObject document) {
         return validate_document_or_file(context, (XmlDocument)document);

programming_language: Java
filename: xml_relax_ng.c
change_type: ModificationType.MODIFY
diff: @@ -53,16 +53,24 @@ static VALUE validate_document(VALUE self, VALUE document)
  *
  * Create a new RelaxNG from the contents of +string+
  */
-static VALUE read_memory(VALUE klass, VALUE content)
+static VALUE read_memory(int argc, VALUE *argv, VALUE klass)
 {
-  xmlRelaxNGParserCtxtPtr ctx = xmlRelaxNGNewMemParserCtxt(
-      (const char *)StringValuePtr(content),
-      (int)RSTRING_LEN(content)
-  );
+  VALUE content;
+  VALUE parse_options;
+  xmlRelaxNGParserCtxtPtr ctx;
   xmlRelaxNGPtr schema;
-  VALUE errors = rb_ary_new();
+  VALUE errors;
   VALUE rb_schema;
+  int scanned_args = 0;
+
+  scanned_args = rb_scan_args(argc, argv, "11", &content, &parse_options);
+  if (scanned_args == 1) {
+    parse_options = rb_const_get(rb_const_get(mNokogiriXml, rb_intern("ParseOptions")), rb_intern("DEFAULT_SCHEMA"));
+  }
 
+  ctx = xmlRelaxNGNewMemParserCtxt((const char *)StringValuePtr(content), (int)RSTRING_LEN(content));
+
+  errors = rb_ary_new();
   xmlSetStructuredErrorFunc((void *)errors, Nokogiri_error_array_pusher);
 
 #ifdef HAVE_XMLRELAXNGSETPARSERSTRUCTUREDERRORS
@@ -90,6 +98,7 @@ static VALUE read_memory(VALUE klass, VALUE content)
 
   rb_schema = Data_Wrap_Struct(klass, 0, dealloc, schema);
   rb_iv_set(rb_schema, "@errors", errors);
+  rb_iv_set(rb_schema, "@parse_options", parse_options);
 
   return rb_schema;
 }
@@ -100,18 +109,25 @@ static VALUE read_memory(VALUE klass, VALUE content)
  *
  * Create a new RelaxNG schema from the Nokogiri::XML::Document +doc+
  */
-static VALUE from_document(VALUE klass, VALUE document)
+static VALUE from_document(int argc, VALUE *argv, VALUE klass)
 {
+  VALUE document;
+  VALUE parse_options;
   xmlDocPtr doc;
   xmlRelaxNGParserCtxtPtr ctx;
   xmlRelaxNGPtr schema;
   VALUE errors;
   VALUE rb_schema;
+  int scanned_args = 0;
+
+  scanned_args = rb_scan_args(argc, argv, "11", &document, &parse_options);
 
   Data_Get_Struct(document, xmlDoc, doc);
+  doc = doc->doc; /* In case someone passes us a node. ugh. */
 
-  /* In case someone passes us a node. ugh. */
-  doc = doc->doc;
+  if (scanned_args == 1) {
+    parse_options = rb_const_get(rb_const_get(mNokogiriXml, rb_intern("ParseOptions")), rb_intern("DEFAULT_SCHEMA"));
+  }
 
   ctx = xmlRelaxNGNewDocParserCtxt(doc);
 
@@ -143,6 +159,7 @@ static VALUE from_document(VALUE klass, VALUE document)
 
   rb_schema = Data_Wrap_Struct(klass, 0, dealloc, schema);
   rb_iv_set(rb_schema, "@errors", errors);
+  rb_iv_set(rb_schema, "@parse_options", parse_options);
 
   return rb_schema;
 }
@@ -156,7 +173,7 @@ void init_xml_relax_ng()
 
   cNokogiriXmlRelaxNG = klass;
 
-  rb_define_singleton_method(klass, "read_memory", read_memory, 1);
-  rb_define_singleton_method(klass, "from_document", from_document, 1);
+  rb_define_singleton_method(klass, "read_memory", read_memory, -1);
+  rb_define_singleton_method(klass, "from_document", from_document, -1);
   rb_define_private_method(klass, "validate_document", validate_document, 1);
 }

programming_language: C
filename: xml_schema.c
change_type: ModificationType.MODIFY
diff: @@ -93,15 +93,26 @@ static VALUE validate_file(VALUE self, VALUE rb_filename)
  *
  * Create a new Schema from the contents of +string+
  */
-static VALUE read_memory(VALUE klass, VALUE content)
+static VALUE read_memory(int argc, VALUE *argv, VALUE klass)
 {
+  VALUE content;
+  VALUE parse_options;
+  int parse_options_int;
+  xmlSchemaParserCtxtPtr ctx;
   xmlSchemaPtr schema;
-  xmlSchemaParserCtxtPtr ctx = xmlSchemaNewMemParserCtxt(
-      (const char *)StringValuePtr(content),
-      (int)RSTRING_LEN(content)
-  );
+  VALUE errors;
   VALUE rb_schema;
-  VALUE errors = rb_ary_new();
+  int scanned_args = 0;
+
+  scanned_args = rb_scan_args(argc, argv, "11", &content, &parse_options);
+  if (scanned_args == 1) {
+    parse_options = rb_const_get(rb_const_get(mNokogiriXml, rb_intern("ParseOptions")), rb_intern("DEFAULT_SCHEMA"));
+  }
+  parse_options_int = (int)NUM2INT(rb_funcall(parse_options, rb_intern("to_i"), 0));
+
+  ctx = xmlSchemaNewMemParserCtxt((const char *)StringValuePtr(content), (int)RSTRING_LEN(content));
+
+  errors = rb_ary_new();
   xmlSetStructuredErrorFunc((void *)errors, Nokogiri_error_array_pusher);
 
 #ifdef HAVE_XMLSCHEMASETPARSERSTRUCTUREDERRORS
@@ -109,7 +120,7 @@ static VALUE read_memory(VALUE klass, VALUE content)
     ctx,
     Nokogiri_error_array_pusher,
     (void *)errors
-  );
+    );
 #endif
 
    schema = xmlSchemaParse(ctx);
@@ -129,6 +140,7 @@ static VALUE read_memory(VALUE klass, VALUE content)
 
   rb_schema = Data_Wrap_Struct(klass, 0, dealloc, schema);
   rb_iv_set(rb_schema, "@errors", errors);
+  rb_iv_set(rb_schema, "@parse_options", parse_options);
 
   return rb_schema;
 }
@@ -164,18 +176,27 @@ static int has_blank_nodes_p(VALUE cache)
  *
  * Create a new Schema from the Nokogiri::XML::Document +doc+
  */
-static VALUE from_document(VALUE klass, VALUE document)
+static VALUE from_document(int argc, VALUE *argv, VALUE klass)
 {
+  VALUE document;
+  VALUE parse_options;
+  int parse_options_int;
   xmlDocPtr doc;
   xmlSchemaParserCtxtPtr ctx;
   xmlSchemaPtr schema;
   VALUE errors;
   VALUE rb_schema;
+  int scanned_args = 0;
+
+  scanned_args = rb_scan_args(argc, argv, "11", &document, &parse_options);
 
   Data_Get_Struct(document, xmlDoc, doc);
+  doc = doc->doc; /* In case someone passes us a node. ugh. */
 
-  /* In case someone passes us a node. ugh. */
-  doc = doc->doc;
+  if (scanned_args == 1) {
+    parse_options = rb_const_get(rb_const_get(mNokogiriXml, rb_intern("ParseOptions")), rb_intern("DEFAULT_SCHEMA"));
+  }
+  parse_options_int = (int)NUM2INT(rb_funcall(parse_options, rb_intern("to_i"), 0));
 
   if (has_blank_nodes_p(DOC_NODE_CACHE(doc))) {
     rb_raise(rb_eArgError, "Creating a schema from a document that has blank nodes exposed to Ruby is dangerous");
@@ -211,6 +232,7 @@ static VALUE from_document(VALUE klass, VALUE document)
 
   rb_schema = Data_Wrap_Struct(klass, 0, dealloc, schema);
   rb_iv_set(rb_schema, "@errors", errors);
+  rb_iv_set(rb_schema, "@parse_options", parse_options);
 
   return rb_schema;
 
@@ -226,8 +248,8 @@ void init_xml_schema()
 
   cNokogiriXmlSchema = klass;
 
-  rb_define_singleton_method(klass, "read_memory", read_memory, 1);
-  rb_define_singleton_method(klass, "from_document", from_document, 1);
+  rb_define_singleton_method(klass, "read_memory", read_memory, -1);
+  rb_define_singleton_method(klass, "from_document", from_document, -1);
 
   rb_define_private_method(klass, "validate_document", validate_document, 1);
   rb_define_private_method(klass, "validate_file",     validate_file, 1);

programming_language: C
filename: parse_options.rb
change_type: ModificationType.MODIFY
diff: @@ -73,6 +73,8 @@ module Nokogiri
       DEFAULT_XML  = RECOVER | NONET
       # the default options used for parsing HTML documents
       DEFAULT_HTML = RECOVER | NOERROR | NOWARNING | NONET
+      # the default options used for parsing XML schemas
+      DEFAULT_SCHEMA = NONET
 
       attr_accessor :options
       def initialize options = STRICT

programming_language: Ruby
filename: relax_ng.rb
change_type: ModificationType.MODIFY
diff: @@ -5,8 +5,8 @@ module Nokogiri
       ###
       # Create a new Nokogiri::XML::RelaxNG document from +string_or_io+.
       # See Nokogiri::XML::RelaxNG for an example.
-      def RelaxNG string_or_io
-        RelaxNG.new(string_or_io)
+      def RelaxNG(string_or_io, options = ParseOptions::DEFAULT_SCHEMA)
+        RelaxNG.new(string_or_io, options)
       end
     end
 

programming_language: Ruby
filename: schema.rb
change_type: ModificationType.MODIFY
diff: @@ -5,8 +5,8 @@ module Nokogiri
       ###
       # Create a new Nokogiri::XML::Schema object using a +string_or_io+
       # object.
-      def Schema string_or_io
-        Schema.new(string_or_io)
+      def Schema(string_or_io, options = ParseOptions::DEFAULT_SCHEMA)
+        Schema.new(string_or_io, options)
       end
     end
 
@@ -30,12 +30,14 @@ module Nokogiri
     class Schema
       # Errors while parsing the schema file
       attr_accessor :errors
+      # The Nokogiri::XML::ParseOptions used to parse the schema
+      attr_accessor :parse_options
 
       ###
       # Create a new Nokogiri::XML::Schema object using a +string_or_io+
       # object.
-      def self.new string_or_io
-        from_document Nokogiri::XML(string_or_io)
+      def self.new string_or_io, options = ParseOptions::DEFAULT_SCHEMA
+        from_document(Nokogiri::XML(string_or_io), options)
       end
 
       ###

programming_language: Ruby
filename: test_relax_ng.rb
change_type: ModificationType.MODIFY
diff: @@ -26,6 +26,40 @@ module Nokogiri
         assert_equal 0, xsd.errors.length
       end
 
+      def test_constructor_method_with_parse_options
+        schema = Nokogiri::XML::RelaxNG(File.read(ADDRESS_SCHEMA_FILE))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::RelaxNG(File.read(ADDRESS_SCHEMA_FILE), Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
+      def test_new_with_parse_options
+        schema = Nokogiri::XML::RelaxNG.new(File.read(ADDRESS_SCHEMA_FILE))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::RelaxNG.new(File.read(ADDRESS_SCHEMA_FILE), Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
+      def test_from_document_with_parse_options
+        schema = Nokogiri::XML::RelaxNG.from_document(Nokogiri::XML::Document.parse(File.read(ADDRESS_SCHEMA_FILE)))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::RelaxNG.from_document(Nokogiri::XML::Document.parse(File.read(ADDRESS_SCHEMA_FILE)),
+                                                      Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
+      def test_read_memory_with_parse_options
+        schema = Nokogiri::XML::RelaxNG.read_memory(File.read(ADDRESS_SCHEMA_FILE))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::RelaxNG.read_memory(File.read(ADDRESS_SCHEMA_FILE),
+                                                    Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
       def test_parse_with_errors
         xml = File.read(ADDRESS_SCHEMA_FILE).sub(/name="/, 'name=')
         assert_raises(Nokogiri::XML::SyntaxError) {

programming_language: Ruby
filename: test_schema.rb
change_type: ModificationType.MODIFY
diff: @@ -109,6 +109,39 @@ module Nokogiri
         assert_instance_of Nokogiri::XML::Schema, xsd
       end
 
+      def test_schema_method_with_parse_options
+        schema = Nokogiri::XML::Schema(File.read(PO_SCHEMA_FILE))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::Schema(File.read(PO_SCHEMA_FILE), Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
+      def test_schema_new_with_parse_options
+        schema = Nokogiri::XML::Schema.new(File.read(PO_SCHEMA_FILE))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::Schema.new(File.read(PO_SCHEMA_FILE), Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
+      def test_schema_from_document_with_parse_options
+        schema = Nokogiri::XML::Schema.from_document(Nokogiri::XML::Document.parse(File.read(PO_SCHEMA_FILE)))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::Schema.from_document(Nokogiri::XML::Document.parse(File.read(PO_SCHEMA_FILE)),
+                                                     Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
+      def test_schema_read_memory_with_parse_options
+        schema = Nokogiri::XML::Schema.read_memory(File.read(PO_SCHEMA_FILE))
+        assert_equal Nokogiri::XML::ParseOptions::DEFAULT_SCHEMA, schema.parse_options
+
+        schema = Nokogiri::XML::Schema.read_memory(File.read(PO_SCHEMA_FILE), Nokogiri::XML::ParseOptions.new.recover)
+        assert_equal Nokogiri::XML::ParseOptions.new.recover, schema.parse_options
+      end
+
       def test_parse_with_io
         xsd = nil
         File.open(PO_SCHEMA_FILE, "rb") { |f|

programming_language: Ruby