CVE-2015-4411 (Ruby, published 2020-02-20T17:15Z)
Description: value: The Moped::BSON::ObjecId.legal? method in mongodb/bson-ruby before 3.0.4 as used in rubygem-moped allows remote attackers to cause a denial of service (worker resource consumption) via a crafted string. NOTE: This issue is due to an incomplete fix to CVE-2015-4410.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-400 \u2014 Uncontrolled Resource Consumption
The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
Repository: https://github.com/mongodb/bson-ruby
Files changed:
ruby/lib/bson.rb
ruby/lib/bson/object_id.rb
ruby/lib/bson/string.rb
ruby/spec/bson/object_id_spec.rb
Commit message: Adding in object id functionality


Vulnerable code:
    # Encode the object_id type
    # @example Encode the object_id.
    # @return [ String ] The encoded object_id.
    # Constant for UTF-8 string encoding.
    #
    # @since 2.0.0
    UTF8 = "UTF-8".freeze

  pending "#to_bson"

Fixed code:

  # Constant for UTF-8 string encoding.
  #
  # @since 2.0.0
  UTF8 = "UTF-8".freeze
require "digest/md5"
require "socket"

    include Comparable
    # Check equality of the object id with another object.
    #
    # @example Check if the object id is equal to the other.
    #   object_id == other
    #
    # @param [ Object ] other The object to check against.
    #
    # @return [ true, false ] If the objects are equal.
    #
    # @since 2.0.0
    def ==(other)
      return false unless other.is_a?(ObjectId)
      to_bson == other.to_bson
    end

    # Check case equality on the object id.
    #
    # @example Check case equality.
    #   object_id === other
    #
    # @param [ Object ] other The object to check against.
    #
    # @return [ true, false ] If the objects are equal in a case.
    #
    # @since 2.0.0
    def ===(other)
      return to_str === other.to_str if other.respond_to?(:to_str)
      super
    end

    # Compare this object id with another object for use in sorting.
    # @example Compare the object id with the other object.
    #   object <=> other
    #
    # @param [ Object ] other The object to compare to.
    #
    # @return [ Integer ] The result of the comparison.
    #
    # @since 2.0.0
    def <=>(other)
      to_bson <=> other.to_bson
    end

    # Return the UTC time at which this ObjectId was generated. This may
    # be used instread of a created_at timestamp since this information
    # is always encoded in the object id.
    #
    # @example Get the generation time.
    #   object_id.generation_time
    #
    # @return [ Time ] The time the id was generated.
    #
    # @since 2.0.0
    def generation_time
      ::Time.at(to_bson.unpack("N")[0]).utc
    end

    # Get the object id as it's raw BSON data.
    #
    # @example Get the raw bson bytes.
    # @note Since Moped's BSON and 10gen BSON before 2.0.0 have different
    #   internal representations, we will attempt to repair the data for cases
    #   where the object was instantiated in a non-standard way. (Like a
    #   Marshal.load)
    #
    # @return [ String ] The raw bytes.
      repair!(@data) if defined?(@data)
      @raw_data ||= @@generator.next
    # Get the string representation of the object id.
    #
    # @example Get the object id as a string.
    #   object_id.to_s
    #
    # @return [ String ] The object id as a string.
    #
    # @since 2.0.0
    def to_s
      to_bson.unpack("H*")[0].force_encoding(UTF8)
    end
    alias :to_str :to_s

    # Raised when trying to create an object id with invalid data.
    #
    # @since 2.0.0
    class Invalid < RuntimeError; end

    private

    def data=(data)
      @raw_data = data
    end

    class << self

      # Create a new object id from raw bytes.
      #
      # @example Create an object id from raw bytes.
      #   BSON::ObjectId.from_data(data)
      #
      # @param [ String ] data The raw bytes.
      #
      # @return [ ObjectId ] The new object id.
      #
      # @since 2.0.0
      def from_data(data)
        object_id = allocate
        object_id.send(:data=, data)
        object_id
      end

      # Create a new object id from a string.
      #
      # @example Create an object id from the string.
      #   BSON::ObjectId.from_string(id)
      #
      # @param [ String ] string The string to create the id from.
      #
      # @raise [ BSON::ObjectId::Invalid ] If the provided string is invalid.
      #
      # @return [ BSON::ObjectId ] The new object id.
      #
      # @since 2.0.0
      def from_string(string)
        unless legal?(string)
          raise Invalid.new("'#{string}' is an invalid ObjectId.")
        end
        from_data([ string ].pack("H*"))
      end

      # Create a new object id from a time.
      #
      # @example Create an object id from a time.
      #   BSON::ObjectId.from_id(time)
      #
      # @example Create an object id from a time, ensuring uniqueness.
      #   BSON::ObjectId.from_id(time, unique: true)
      #
      # @param [ Time ] time The time to generate from.
      # @param [ Hash ] options The options.
      #
      # @option options [ true, false ] :unique Whether the id should be
      #   unique.
      #
      # @return [ ObjectId ] The new object id.
      #
      # @since 2.0.0
      def from_time(time, options = {})
        from_data(options[:unique] ? @@generator.next(time.to_i) : [ time.to_i ].pack("Nx8"))
      end

      # Determine if the provided string is a legal object id.
      #
      # @example Is the string a legal object id?
      #   BSON::ObjectId.legal?(string)
      #
      # @param [ String ] The string to check.
      #
      # @return [ true, false ] If the string is legal.
      #
      # @since 2.0.0
      def legal?(string)
        /\A\h{24}\Z/ === string.to_s
      end
    end

    # Inner class that encapsulates the behaviour of actually generating each
    # part of the ObjectId.
    #
    # @api private
    #
    # @since 2.0.0
    class Generator

      attr_reader :machine_id

      # Instantiate the new object id generator. Will set the machine id once
      # on the initial instantiation.
      #
      # @example Instantiate the generator.
      #   BSON::ObjectId::Generator.new
      #
      # @since 2.0.0
      def initialize
        @counter = 0
        @machine_id = Digest::MD5.digest(Socket.gethostname).unpack("N")[0]
        @mutex = Mutex.new
      end

      # Return object id data based on the current time, incrementing the
      # object id counter. Will use the provided time if not nil.
      #
      # @example Get the next object id data.
      #   generator.next
      #
      # @param [ Time ] time The optional time to generate with.
      #
      # @return [ String ] The raw object id bytes.
      #
      # @since 2.0.0
      def next(time = nil)
        @mutex.lock
        begin
          count = @counter = (@counter + 1) % 0xFFFFFF
        ensure
          @mutex.unlock rescue nil
        end
        generate(time || ::Time.new.to_i, count)
      end

      # Generate object id data for a given time using the provided counter.
      #
      # @example Generate the object id bytes.
      #   generator.generate(time)
      #
      # @param [ Integer ] time The time since epoch in seconds.
      # @param [ Integer ] counter The optional counter.
      #
      # @return [ String ] The raw object id bytes.
      #
      # @since 2.0.0
      def generate(time, counter = 0)
        process_thread_id = "#{Process.pid}#{Thread.current.object_id}".hash % 0xFFFF
        [ time, machine_id, process_thread_id, counter << 8 ].pack("N NX lXX NX")
      end
    end

    # We keep one global generator for object ids.
    #
    # @since 2.0.0
    @@generator = Generator.new

  describe "#==" do

    context "when data is identical" do

      let(:time) do
        Time.now
      end

      let(:object_id) do
        described_class.from_time(time)
      end

      let(:other_id) do
        described_class.from_time(time)
      end

      it "returns true" do
        expect(object_id).to eq(other_id)
      end
    end

    context "when the data is different" do

      let(:time) do
        Time.now
      end

      let(:object_id) do
        described_class.from_time(time)
      end

      it "returns false" do
        expect(object_id).to_not eq(described_class.new)
      end
    end

    context "when other is not an object id" do

      it "returns false" do
        expect(described_class.new).to_not eq(nil)
      end
    end
  end

  describe "#===" do

    let(:object_id) do
      described_class.new
    end

    context "when comparing with another object id" do

      context "when the data is equal" do

        let(:other) do
          described_class.from_string(object_id.to_s)
        end

        it "returns true" do
          expect(object_id === other).to be_true
        end
      end

      context "when the data is not equal" do

        let(:other) do
          described_class.new
        end

        it "returns false" do
          expect(object_id === other).to be_false
        end
      end
    end

    context "when comparing to an object id class" do

      it "returns false" do
        expect(object_id === BSON::ObjectId).to be_false
      end
    end

    context "when comparing with a string" do

      context "when the data is equal" do

        let(:other) do
          object_id.to_s
        end

        it "returns true" do
          expect(object_id === other).to be_true
        end
      end

      context "when the data is not equal" do

        let(:other) do
          described_class.new.to_s
        end

        it "returns false" do
          expect(object_id === other).to be_false
        end
      end
    end

    context "when comparing with a non string or object id" do

      it "returns false" do
        expect(object_id === "test").to be_false
      end
    end

    context "when comparing with a non object id class" do

      it "returns false" do
        expect(object_id === String).to be_false
      end
    end
  end

  describe "#<" do

    let(:object_id) do
      described_class.from_time(Time.new(2012, 1, 1))
    end

    let(:other_id) do
      described_class.from_time(Time.new(2012, 1, 30))
    end

    context "when the generation time before the other" do

      it "returns true" do
        expect(object_id < other_id).to be_true
      end
    end

    context "when the generation time is after the other" do

      it "returns false" do
        expect(other_id < object_id).to be_false
      end
    end
  end

  describe "#>" do

    let(:object_id) do
      described_class.from_time(Time.new(2012, 1, 1))
    end

    let(:other_id) do
      described_class.from_time(Time.new(2012, 1, 30))
    end

    context "when the generation time before the other" do

      it "returns false" do
        expect(object_id > other_id).to be_false
      end
    end

    context "when the generation time is after the other" do

      it "returns true" do
        expect(other_id > object_id).to be_true
      end
    end
  end

  describe "#<=>" do

    let(:object_id) do
      described_class.from_time(Time.new(2012, 1, 1))
    end

    let(:other_id) do
      described_class.from_time(Time.new(2012, 1, 30))
    end

    context "when the generation time before the other" do

      it "returns -1" do
        expect(object_id <=> other_id).to eq(-1)
      end
    end

    context "when the generation time is after the other" do

      it "returns false" do
        expect(other_id <=> object_id).to eq(1)
      end
    end
  end

  describe ".from_string" do

    context "when the string is valid" do

      let(:string) do
        "4e4d66343b39b68407000001"
      end

      let(:object_id) do
        described_class.from_string(string)
      end

      it "initializes with the string's bytes" do
        expect(object_id.to_s).to eq(string)
      end
    end

    context "when the string is not valid" do

      it "raises an error" do
        expect {
          described_class.from_string("asadsf")
        }.to raise_error(BSON::ObjectId::Invalid)
      end
    end
  end

  describe ".from_time" do

    context "when no unique option is provided" do

      let(:time) do
        Time.at((Time.now.utc - 64800).to_i).utc
      end

      let(:object_id) do
        described_class.from_time(time)
      end

      it "sets the generation time" do
        expect(object_id.generation_time).to eq(time)
      end

      it "does not include process or sequence information" do
        expect(object_id.to_s =~ /\A\h{8}0{16}\Z/).to be_true
      end
    end

    context "when a unique option is provided" do

      let(:time) do
        Time.at((Time.now.utc - 64800).to_i).utc
      end

      let(:object_id) do
        described_class.from_time(time, unique: true)
      end

      let(:non_unique) do
        described_class.from_time(time, unique: true)
      end

      it "creates a new unique object id" do
        expect(object_id).to_not eq(non_unique)
      end
    end
  end

  describe ".legal?" do

    context "when the string is too short to be an object id" do

      it "returns false" do
        expect(described_class).to_not be_legal("a" * 23)
      end
    end

    context "when the string contains invalid hex characters" do

      it "returns false" do
        expect(described_class).to_not be_legal("y" + "a" * 23)
      end
    end

    context "when the string is a valid object id" do

      it "returns true" do
        expect(described_class).to be_legal("a" * 24)
      end
    end

    context "when checking against another object id" do

      let(:object_id) do
        described_class.new
      end

      it "returns true" do
        expect(described_class).to be_legal(object_id)
      end
    end
  end

  describe "#to_bson" do

    let(:object_id) do
      described_class.new
    end

    let(:data) do
      object_id.instance_variable_get(:@raw_data)
    end

    it "returns the raw bytes" do
      expect(object_id.to_bson).to eq(data)
    end
  end

  describe "#to_s" do

    let(:time) do
      Time.new(2013, 1, 1)
    end

    let(:expected) do
      "50e218f00000000000000000"
    end

    let(:object_id) do
      described_class.from_time(time)
    end

    it "returns a hex string representation of the id" do
      expect(object_id.to_s).to eq(expected)
    end

    it "returns the string in UTF-8" do
      expect(object_id.to_s.encoding).to eq(Encoding.find(BSON::UTF8))
    end

    it "converts to a readable yaml string" do
      expect(YAML.dump(object_id.to_s)).to include(expected)
    end
  end