CVE-2020-26247 (Ruby, published 2020-12-30T19:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 4.3)
Weakness: CWE-611 \u2014 Improper Restriction of XML External Entity Reference
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.
Repository: https://github.com/sparklemotion/nokogiri
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 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.


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