summaryrefslogtreecommitdiff
path: root/lib/gitlab/middleware/handle_null_bytes.rb
blob: c88dfb6ee0bddb786d6973302809a31b48481889 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# frozen_string_literal: true

module Gitlab
  module Middleware
    # There is no valid reason for a request to contain a null byte (U+0000)
    # so just return HTTP 400 (Bad Request) if we receive one
    class HandleNullBytes
      NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze

      attr_reader :app

      def initialize(app)
        @app = app
      end

      def call(env)
        return [400, {}, ["Bad Request"]] if request_has_null_byte?(env)

        app.call(env)
      end

      private

      def request_has_null_byte?(request)
        return false if ENV['REJECT_NULL_BYTES'] == "1"

        request = Rack::Request.new(request)

        request.params.values.any? do |value|
          param_has_null_byte?(value)
        end
      end

      def param_has_null_byte?(value, depth = 0)
        # Guard against possible attack sending large amounts of nested params
        # Should be safe as deeply nested params are highly uncommon.
        return false if depth > 2

        depth += 1

        if value.respond_to?(:match)
          string_contains_null_byte?(value)
        elsif value.respond_to?(:values)
          value.values.any? do |hash_value|
            param_has_null_byte?(hash_value, depth)
          end
        elsif value.is_a?(Array)
          value.any? do |array_value|
            param_has_null_byte?(array_value, depth)
          end
        else
          false
        end
      end

      def string_contains_null_byte?(string)
        string.match?(NULL_BYTE_REGEX)
      end
    end
  end
end