summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/middleware/handle_malformed_strings_spec.rb
blob: ed1440f23b6e1499065eb32de5d66977ac2c83ee (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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
# frozen_string_literal: true
require 'spec_helper'
require "rack/test"

RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
  include GitHttpHelpers

  let(:null_byte) { "\u0000" }
  let(:escaped_null_byte) { "%00" }
  let(:invalid_string) { "mal\xC0formed" }
  let(:escaped_invalid_string) { "mal%c0formed" }
  let(:error_400) { [400, { 'Content-Type' => 'text/plain' }, ['Bad Request']] }
  let(:app) { double(:app) }

  subject { described_class.new(app) }

  before do
    allow(app).to receive(:call) do |args|
      args
    end
  end

  def env_for(params = {})
    Rack::MockRequest.env_for('/', { params: params })
  end

  context 'in the URL' do
    it 'rejects null bytes' do
      # We have to create the env separately or Rack::MockRequest complains about invalid URI
      env = env_for
      env['PATH_INFO'] = "/someplace/witha#{null_byte}nullbyte"

      expect(subject.call(env)).to eq error_400
    end

    it 'rejects escaped null bytes' do
      # We have to create the env separately or Rack::MockRequest complains about invalid URI
      env = env_for
      env['PATH_INFO'] = "/someplace/withan#{escaped_null_byte}escaped nullbyte"

      expect(subject.call(env)).to eq error_400
    end

    it 'rejects malformed strings' do
      # We have to create the env separately or Rack::MockRequest complains about invalid URI
      env = env_for
      env['PATH_INFO'] = "/someplace/with_an/#{invalid_string}"

      expect(subject.call(env)).to eq error_400
    end

    it 'rejects escaped malformed strings' do
      # We have to create the env separately or Rack::MockRequest complains about invalid URI
      env = env_for
      env['PATH_INFO'] = "/someplace/with_an/#{escaped_invalid_string}"

      expect(subject.call(env)).to eq error_400
    end
  end

  context 'in authorization headers' do
    let(:problematic_input) { null_byte }

    shared_examples 'rejecting invalid input' do
      it 'rejects problematic input in the password' do
        env = env_for.merge(auth_env("username", "password#{problematic_input}encoded", nil))

        expect(subject.call(env)).to eq error_400
      end

      it 'rejects problematic input in the username' do
        env = env_for.merge(auth_env("username#{problematic_input}", "passwordencoded", nil))

        expect(subject.call(env)).to eq error_400
      end

      it 'rejects problematic input in non-basic-auth tokens' do
        env = env_for.merge('HTTP_AUTHORIZATION' => "GL-Geo hello#{problematic_input}world")

        expect(subject.call(env)).to eq error_400
      end
    end

    it_behaves_like 'rejecting invalid input' do
      let(:problematic_input) { null_byte }
    end

    it_behaves_like 'rejecting invalid input' do
      let(:problematic_input) { invalid_string }
    end

    it_behaves_like 'rejecting invalid input' do
      let(:problematic_input) { "\xC3" }
    end

    it 'does not reject correct non-basic-auth tokens' do
      # This token is known to include a null-byte when we were to try to decode it
      # as Base64, while it wasn't encoded at such.
      special_token = 'GL-Geo ta8KakZWpu0AcledQ6n0:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoie1wic2NvcGVcIjpcImdlb19hcGlcIn0iLCJqdGkiOiIwYWFmNzVlYi1lNWRkLTRkZjEtODQzYi1lM2E5ODhhNDMwMzIiLCJpYXQiOjE2MDQ3MDI4NzUsIm5iZiI6MTYwNDcwMjg3MCwiZXhwIjoxNjA0NzAyOTM1fQ.NcgDipDyxSP5uSzxc01ylzH4GkTxJRflNNjT7U6fpg4'
      expect(Base64.decode64(special_token)).to include(null_byte)

      env = env_for.merge('HTTP_AUTHORIZATION' => special_token)

      expect(subject.call(env)).not_to eq error_400
    end

    it 'does not reject correct encoded password with special characters' do
      env = env_for.merge(auth_env("username", "RçKszEwéC5kFnû∆f243fycGu§Gh9ftDj!U", nil))

      expect(subject.call(env)).not_to eq error_400
    end
  end

  context 'in params' do
    shared_examples_for 'checks params' do
      it 'rejects bad params in a top level param' do
        env = env_for(name: "null#{problematic_input}byte")

        expect(subject.call(env)).to eq error_400
      end

      it "rejects bad params for hashes with strings" do
        env = env_for(name: { inner_key: "I am #{problematic_input} bad" })

        expect(subject.call(env)).to eq error_400
      end

      it "rejects bad params for arrays with strings" do
        env = env_for(name: ["I am #{problematic_input} bad"])

        expect(subject.call(env)).to eq error_400
      end

      it "rejects bad params for arrays containing hashes with string values" do
        env = env_for(
          name: [
            {
              inner_key: "I am #{problematic_input} bad"
            }
          ])

        expect(subject.call(env)).to eq error_400
      end
    end

    context 'with null byte' do
      let(:problematic_input) { null_byte }

      it_behaves_like 'checks params'

      it "gives up and does not reject too deeply nested params" do
        env = env_for(
          name: [
            {
              inner_key: { deeper_key: [{ hash_inside_array_key: "I am #{problematic_input} bad" }] }
            }
          ])

        expect(subject.call(env)).not_to eq error_400
      end
    end

    context 'with malformed strings' do
      it_behaves_like 'checks params' do
        let(:problematic_input) { invalid_string }
      end
    end
  end

  context 'without problematic input' do
    it "does not error for strings" do
      env = env_for(name: "safe name")

      expect(subject.call(env)).not_to eq error_400
    end

    it "does not error with no params" do
      env = env_for

      expect(subject.call(env)).not_to eq error_400
    end
  end

  it 'does not modify the env' do
    env = env_for

    expect { subject.call(env) }.not_to change { env }
  end
end