CVE-2020-26247: Improper Restriction of XML External Entity Reference (CWE-611)
value: 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.
CWE 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.
CVSS v3 base score: 4.3
Published: 2020-12-30T19:15Z
Repository: https://github.com/sparklemotion/nokogiri
Language: Ruby
Files changed: ext/java/nokogiri/XmlRelaxng.java, ext/java/nokogiri/XmlSchema.java, ext/nokogiri/xml_relax_ng.c, ext/nokogiri/xml_schema.c, lib/nokogiri/xml/parse_options.rb, lib/nokogiri/xml/relax_ng.rb, lib/nokogiri/xml/schema.rb, test/xml/test_relax_ng.rb, test/xml/test_schema.rb
Commit date: 2020-12-03 14:08:11 -0500
Commit message: 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.

Diff: diff --git a/ext/java/nokogiri/XmlRelaxng.java b/ext/java/nokogiri/XmlRelaxng.java
index 4b0d73f4..e57aa83d 100644
--- a/ext/java/nokogiri/XmlRelaxng.java
+++ b/ext/java/nokogiri/XmlRelaxng.java
@@ -54,10 +54,11 @@ import org.iso_relax.verifier.VerifierConfigurationException;
 import org.iso_relax.verifier.VerifierFactory;
 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;
 
 /**
@@ -76,15 +77,21 @@ public class XmlRelaxng extends XmlSchema {
     
     private void setVerifier(Verifier verifier) {
         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());
             return xmlRelaxng;
         } catch (VerifierConfigurationException ex) {
diff --git a/ext/java/nokogiri/XmlSchema.java b/ext/java/nokogiri/XmlSchema.java
index b030fb84..f8460951 100644
--- a/ext/java/nokogiri/XmlSchema.java
+++ b/ext/java/nokogiri/XmlSchema.java
@@ -104,14 +104,20 @@ public class XmlSchema extends RubyObject {
 
     private void setValidator(Validator validator) {
         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"));
             Schema schema = xmlSchema.getSchema(source, context.getRuntime().getCurrentDirectory(), context.getRuntime().getInstanceConfig().getScriptFileName(), error_handler);
             xmlSchema.setValidator(schema.newValidator());
@@ -119,18 +125,28 @@ public class XmlSchema extends RubyObject {
         } catch (SAXException ex) {
             throw context.getRuntime().newRuntimeError("Could not parse document: " + ex.getMessage());
         }
     }
 
+    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");
         if (!errors.isEmpty()) {
             throw ((XmlSyntaxError) errors.first()).toThrowable();
@@ -142,29 +158,34 @@ public class XmlSchema extends RubyObject {
 
         if (!uri.isNil()) {
             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);
     }
 
diff --git a/ext/nokogiri/xml_relax_ng.c b/ext/nokogiri/xml_relax_ng.c
index cb80bdb0..9e62ff39 100644
--- a/ext/nokogiri/xml_relax_ng.c
+++ b/ext/nokogiri/xml_relax_ng.c
@@ -51,20 +51,28 @@ static VALUE validate_document(VALUE self, VALUE document)
  * call-seq:
  *  read_memory(string)
  *
  * 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
   xmlRelaxNGSetParserStructuredErrors(
     ctx,
@@ -88,32 +96,40 @@ static VALUE read_memory(VALUE klass, VALUE content)
     return Qnil;
   }
 
   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;
 }
 
 /*
  * call-seq:
  *  from_document(doc)
  *
  * 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);
 
   errors = rb_ary_new();
   xmlSetStructuredErrorFunc((void *)errors, Nokogiri_error_array_pusher);
@@ -141,10 +157,11 @@ static VALUE from_document(VALUE klass, VALUE document)
     return Qnil;
   }
 
   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;
 }
 
 VALUE cNokogiriXmlRelaxNG;
@@ -154,9 +171,9 @@ void init_xml_relax_ng()
   VALUE xml = rb_define_module_under(nokogiri, "XML");
   VALUE klass = rb_define_class_under(xml, "RelaxNG", cNokogiriXmlSchema);
 
   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);
 }
diff --git a/ext/nokogiri/xml_schema.c b/ext/nokogiri/xml_schema.c
index 439f7219..ea7c3d31 100644
--- a/ext/nokogiri/xml_schema.c
+++ b/ext/nokogiri/xml_schema.c
@@ -91,27 +91,38 @@ static VALUE validate_file(VALUE self, VALUE rb_filename)
  * call-seq:
  *  read_memory(string)
  *
  * 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
   xmlSchemaSetParserStructuredErrors(
     ctx,
     Nokogiri_error_array_pusher,
     (void *)errors
-  );
+    );
 #endif
 
    schema = xmlSchemaParse(ctx);
 
   xmlSetStructuredErrorFunc(NULL, NULL);
@@ -127,10 +138,11 @@ static VALUE read_memory(VALUE klass, VALUE content)
     return Qnil;
   }
 
   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;
 }
 
 /* Schema creation will remove and deallocate "blank" nodes.
@@ -162,22 +174,31 @@ static int has_blank_nodes_p(VALUE cache)
  * call-seq:
  *  from_document(doc)
  *
  * 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");
   }
 
@@ -209,10 +230,11 @@ static VALUE from_document(VALUE klass, VALUE document)
     return Qnil;
   }
 
   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;
 
   return Qnil;
 }
@@ -224,11 +246,11 @@ void init_xml_schema()
   VALUE xml = rb_define_module_under(nokogiri, "XML");
   VALUE klass = rb_define_class_under(xml, "Schema", rb_cObject);
 
   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);
 }
diff --git a/lib/nokogiri/xml/parse_options.rb b/lib/nokogiri/xml/parse_options.rb
index 039afa2d..a266d5ba 100644
--- a/lib/nokogiri/xml/parse_options.rb
+++ b/lib/nokogiri/xml/parse_options.rb
@@ -71,10 +71,12 @@ module Nokogiri
 
       # the default options used for parsing XML documents
       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
         @options = options
       end
diff --git a/lib/nokogiri/xml/relax_ng.rb b/lib/nokogiri/xml/relax_ng.rb
index 4d9ad65d..b1e83efb 100644
--- a/lib/nokogiri/xml/relax_ng.rb
+++ b/lib/nokogiri/xml/relax_ng.rb
@@ -3,12 +3,12 @@ module Nokogiri
   module XML
     class << self
       ###
       # 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
 
     ###
     # Nokogiri::XML::RelaxNG is used for validating XML against a
diff --git a/lib/nokogiri/xml/schema.rb b/lib/nokogiri/xml/schema.rb
index 60f1b2d3..b3719d7d 100644
--- a/lib/nokogiri/xml/schema.rb
+++ b/lib/nokogiri/xml/schema.rb
@@ -3,12 +3,12 @@ module Nokogiri
   module XML
     class << self
       ###
       # 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
 
     ###
     # Nokogiri::XML::Schema is used for validating XML against a schema
@@ -28,16 +28,18 @@ module Nokogiri
     #
     # The list of errors are Nokogiri::XML::SyntaxError objects.
     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
 
       ###
       # Validate +thing+ against this schema.  +thing+ can be a
       # Nokogiri::XML::Document object, or a filename.  An Array of
diff --git a/test/xml/test_relax_ng.rb b/test/xml/test_relax_ng.rb
index 23ede368..02fd91b0 100644
--- a/test/xml/test_relax_ng.rb
+++ b/test/xml/test_relax_ng.rb
@@ -24,10 +24,44 @@ module Nokogiri
           assert xsd = Nokogiri::XML::RelaxNG(f)
         }
         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) {
           Nokogiri::XML::RelaxNG(xml)
         }
diff --git a/test/xml/test_schema.rb b/test/xml/test_schema.rb
index 908c7c18..2bd267b9 100644
--- a/test/xml/test_schema.rb
+++ b/test/xml/test_schema.rb
@@ -107,10 +107,43 @@ module Nokogiri
       def test_new
         assert xsd = Nokogiri::XML::Schema.new(File.read(PO_SCHEMA_FILE))
         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|
           assert xsd = Nokogiri::XML::Schema(f)
         }

Vulnerable code:     static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source) {
        
    static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source) {
    @JRubyMethod(meta=true)
    public static IRubyObject from_document(ThreadContext context, IRubyObject klazz, IRubyObject document) {
        return getSchema(context, (RubyClass)klazz, source);
    private static IRubyObject getSchema(ThreadContext context, RubyClass klazz, Source source) {
            return XmlSchema.createSchemaInstance(context, klazz, source);
            return XmlRelaxng.createSchemaInstance(context, klazz, source);
    @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)));
    }

static VALUE read_memory(VALUE klass, VALUE content)
  xmlRelaxNGParserCtxtPtr ctx = xmlRelaxNGNewMemParserCtxt(
      (const char *)StringValuePtr(content),
      (int)RSTRING_LEN(content)
  );
  VALUE errors = rb_ary_new();
static VALUE from_document(VALUE klass, VALUE document)
  /* In case someone passes us a node. ugh. */
  doc = doc->doc;
  rb_define_singleton_method(klass, "read_memory", read_memory, 1);
  rb_define_singleton_method(klass, "from_document", from_document, 1);
static VALUE read_memory(VALUE klass, VALUE content)
  xmlSchemaParserCtxtPtr ctx = xmlSchemaNewMemParserCtxt(
      (const char *)StringValuePtr(content),
      (int)RSTRING_LEN(content)
  );
  VALUE errors = rb_ary_new();
  );
static VALUE from_document(VALUE klass, VALUE document)
  /* In case someone passes us a node. ugh. */
  doc = doc->doc;
  rb_define_singleton_method(klass, "read_memory", read_memory, 1);
  rb_define_singleton_method(klass, "from_document", from_document, 1);
      def RelaxNG string_or_io
        RelaxNG.new(string_or_io)
      def Schema string_or_io
        Schema.new(string_or_io)
      def self.new string_or_io
        from_document Nokogiri::XML(string_or_io)
Fixed code: import org.jruby.runtime.builtin.IRubyObject;
    static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source, IRubyObject parseOptions) {

        if (parseOptions == null) {
            parseOptions = defaultParseOptions(context.getRuntime());
        }

        xmlRelaxng.setInstanceVariable("@parse_options", parseOptions);

    static XmlSchema createSchemaInstance(ThreadContext context, RubyClass klazz, Source source, IRubyObject parseOptions) {

        if (parseOptions == null) {
            parseOptions = defaultParseOptions(context.getRuntime());
        }

        xmlSchema.setInstanceVariable("@parse_options", parseOptions);
    protected static IRubyObject defaultParseOptions(Ruby runtime) {
        return ((RubyClass)runtime.getClassFromPath("Nokogiri::XML::ParseOptions")).getConstant("DEFAULT_SCHEMA");
    }

    @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];
        }

        return getSchema(context, (RubyClass)klazz, source, parseOptions);
    @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) {
            return XmlSchema.createSchemaInstance(context, klazz, source, parseOptions);
            return XmlRelaxng.createSchemaInstance(context, klazz, source, parseOptions);
static VALUE read_memory(int argc, VALUE *argv, VALUE klass)
  VALUE content;
  VALUE parse_options;
  xmlRelaxNGParserCtxtPtr ctx;
  VALUE errors;
  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();
  rb_iv_set(rb_schema, "@parse_options", parse_options);
static VALUE from_document(int argc, VALUE *argv, VALUE klass)
  VALUE document;
  VALUE parse_options;
  int scanned_args = 0;

  scanned_args = rb_scan_args(argc, argv, "11", &document, &parse_options);
  doc = doc->doc; /* In case someone passes us a node. ugh. */
  if (scanned_args == 1) {
    parse_options = rb_const_get(rb_const_get(mNokogiriXml, rb_intern("ParseOptions")), rb_intern("DEFAULT_SCHEMA"));
  }
  rb_iv_set(rb_schema, "@parse_options", parse_options);
  rb_define_singleton_method(klass, "read_memory", read_memory, -1);
  rb_define_singleton_method(klass, "from_document", from_document, -1);
static VALUE read_memory(int argc, VALUE *argv, VALUE klass)
  VALUE content;
  VALUE parse_options;
  int parse_options_int;
  xmlSchemaParserCtxtPtr ctx;
  VALUE errors;
  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();
    );
  rb_iv_set(rb_schema, "@parse_options", parse_options);
static VALUE from_document(int argc, VALUE *argv, VALUE klass)
  VALUE document;
  VALUE parse_options;
  int parse_options_int;
  int scanned_args = 0;

  scanned_args = rb_scan_args(argc, argv, "11", &document, &parse_options);
  doc = doc->doc; /* In case someone passes us a node. ugh. */
  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));
  rb_iv_set(rb_schema, "@parse_options", parse_options);
  rb_define_singleton_method(klass, "read_memory", read_memory, -1);
  rb_define_singleton_method(klass, "from_document", from_document, -1);
      # the default options used for parsing XML schemas
      DEFAULT_SCHEMA = NONET
      def RelaxNG(string_or_io, options = ParseOptions::DEFAULT_SCHEMA)
        RelaxNG.new(string_or_io, options)
      def Schema(string_or_io, options = ParseOptions::DEFAULT_SCHEMA)
        Schema.new(string_or_io, options)
      # The Nokogiri::XML::ParseOptions used to parse the schema
      attr_accessor :parse_options
      def self.new string_or_io, options = ParseOptions::DEFAULT_SCHEMA
        from_document(Nokogiri::XML(string_or_io), options)
      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_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