sinatra-my-params 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8391c458014310cc388fc93e899320baa879567e50ca8cb875e3a19e20ae5984
4
- data.tar.gz: a928880e81d7f1cfb6d91d2da10347398f8eca11b85bf808eca3c12af8f7a675
3
+ metadata.gz: 6b1f2c6fc4b59ce732264ff359866988b740c93b3dd585c43dcd57ee0834a8a1
4
+ data.tar.gz: b9f6692a739211d236c32b1658a66dd71b2f0e4e40ecb22010d63031bc7c03a8
5
5
  SHA512:
6
- metadata.gz: c18faa501799088dcf64962e616789b38a78176f4b0fdfc3b0c123e157b3f799ee7638243d6fb522ab55de945d296206d0c71e46436c4ff30444e5ee95ba8300
7
- data.tar.gz: 257127aa4473a46ee13031cef5251f9b94cbb7195444130e0cb6011fc25376bab83a09e52a5c5b03c1e2e02e7a923d3fe942702c88370b65824b3ecf87fb3a11
6
+ metadata.gz: 906f99e74e6bede4cbf4ee24dbf281a08d1c4c0856ea18a2434e445c01f0f028aa5b165e8a3219716043b4be82141744e2a10894b5aada1f677c9e0960d504cc
7
+ data.tar.gz: a07cdd0764f4f0c39bf83c17aca176204d9469ebc2583bbba7804a12d55f3fc60e8f07896536df5a7fc4f8961082ed91d05e1da8e9a9a9a2cb05ecd8bcc0557c
data/lib/permit_params.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'date'
4
+ require 'time'
5
+
3
6
  module PermitParams
4
7
  class InvalidParameterError < StandardError
5
8
  attr_accessor :param, :options
@@ -8,7 +11,7 @@ module PermitParams
8
11
  def permitted_params(params, permitted = {}, strong_validation = false, options = {})
9
12
  return params if permitted.empty?
10
13
 
11
- coerced_params = Hash.new({})
14
+ coerced_params = {}
12
15
 
13
16
  params.each do |key, value|
14
17
  next unless permitted?(permitted: permitted, key: key, value: value)
@@ -30,6 +33,14 @@ module PermitParams
30
33
  Any = :any
31
34
  Shape = :shape
32
35
 
36
+ # Mirrors the fix Ruby itself shipped for CVE-2021-41817 (ReDoS in
37
+ # Date/Time/DateTime parsing methods): never hand an unbounded,
38
+ # attacker-controlled string to a parser that uses backtracking regexes.
39
+ PARSE_LENGTH_LIMIT = 128
40
+
41
+ DATE_TIME_TYPES = [Date, Time, DateTime].freeze
42
+ NUMERIC_PARSE_TYPES = [Integer, Float].freeze
43
+
33
44
  def permitted?(permitted:, key:, value:)
34
45
  permitted.keys.map(&:to_s).include?(key.to_s) && !value.nil?
35
46
  end
@@ -44,6 +55,11 @@ module PermitParams
44
55
  rescue StandardError
45
56
  false
46
57
  end
58
+
59
+ if param.is_a?(String) && (DATE_TIME_TYPES.include?(type) || NUMERIC_PARSE_TYPES.include?(type))
60
+ raise ArgumentError, "'#{type}' input exceeds #{PARSE_LENGTH_LIMIT} bytes" if param.bytesize > PARSE_LENGTH_LIMIT
61
+ end
62
+
47
63
  return coerce_integer(param, options) if type == Integer
48
64
  return Float(param) if type == Float
49
65
  return String(param) if type == String
@@ -56,8 +72,14 @@ module PermitParams
56
72
  return coerce_boolean(param) if [TrueClass, FalseClass, Boolean].include? type
57
73
 
58
74
  nil
59
- rescue ArgumentError
75
+ rescue StandardError
76
+ # Any failure while coercing untrusted input (malformed value,
77
+ # unexpected nested shape, missing stdlib constant, etc.) must never
78
+ # crash the caller's request handler - it's either rejected loudly
79
+ # (strong_validation) or dropped silently, same as an invalid value.
60
80
  raise InvalidParameterError, "'#{param}' is not a valid #{type}" if strong_validation
81
+
82
+ nil
61
83
  end
62
84
  end
63
85
 
@@ -66,35 +88,64 @@ module PermitParams
66
88
  end
67
89
 
68
90
  def coerce_array(param, options = {})
69
- Array(param.split(options[:delimiter] || ',').map(&:strip))
91
+ delimiter = valid_delimiter?(param, options[:delimiter])
92
+ return unless delimiter
93
+
94
+ Array(param.split(delimiter).map(&:strip))
70
95
  end
71
96
 
72
97
  def coerce_hash(param, options = {})
73
98
  return param if param.is_a?(Hash)
74
99
 
75
- key_value = param.split(options[:delimiter] || ',').map(&:strip).map do |c|
76
- c.split(options[:separator] || ':').map(&:strip)
100
+ delimiter = valid_delimiter?(param, options[:delimiter])
101
+ return unless delimiter
102
+
103
+ separator = valid_separator?(param, options[:separator])
104
+ return unless separator
105
+
106
+ key_value = param.split(delimiter).map(&:strip).map do |c|
107
+ c.split(separator).map(&:strip)
77
108
  end
78
109
  Hash[key_value]
79
110
  end
80
111
 
81
112
  def coerce_boolean(param)
82
- coerced = if /^(false|f|no|n|0)$/i === param.to_s
113
+ # \A/\z (not ^/$) anchor to the start/end of the *whole string*. In Ruby
114
+ # ^ and $ only anchor to line boundaries, so e.g. "true\nDROP TABLE..."
115
+ # would incorrectly match as a clean boolean with ^/$.
116
+ coerced = if /\A(false|f|no|n|0)\z/i === param.to_s
83
117
  false
84
118
  else
85
- /^(true|t|yes|y|1)$/i === param.to_s ? true : nil
119
+ /\A(true|t|yes|y|1)\z/i === param.to_s ? true : nil
86
120
  end
87
121
  raise ArgumentError if coerced.nil?
88
122
 
89
123
  coerced
90
124
  end
91
125
 
126
+ def valid_delimiter?(param, delimiter)
127
+ delimiter ||= ','
128
+ delimiter if delimiter && param.include?(delimiter)
129
+ end
130
+
131
+ def valid_separator?(param, separator)
132
+ separator ||= ':'
133
+ separator if separator && param.include?(separator)
134
+ end
135
+
92
136
  def coerce_shape(param, options = {})
93
- hash = coerce_hash(param)
137
+ hash = coerce_hash(param, options)
94
138
  has_shape?(hash, options[:shape]) ? hash : nil
95
139
  end
96
140
 
97
141
  def has_shape?(hash, shape)
142
+ # `hash` can be nil (coerce_hash gave up on a malformed string) and
143
+ # `shape` can be nil/non-Hash (caller forgot to pass shape:, or an
144
+ # attacker sent an unexpected nested hash the shape never described).
145
+ # Either used to raise NoMethodError deep inside a supposedly "safe"
146
+ # input filter, crashing the whole request. Reject instead of raising.
147
+ return false unless hash.is_a?(Hash) && shape.is_a?(Hash)
148
+
98
149
  hash.all? do |k, v|
99
150
  v.is_a?(Hash) ? has_shape?(v, shape[k]) : shape[k] === v
100
151
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test/unit'
4
+ require 'permit_params'
5
+ require 'rspec'
6
+ require 'rack/test'
7
+
8
+ include PermitParams
9
+
10
+ describe 'shape behaviours' do
11
+ it 'should return a hash when a pemitted is of shape' do
12
+ input = { param: { a: 1, b: 2 } }
13
+ expect(input).to eq permitted_params(
14
+ input,
15
+ { param: Shape },
16
+ false,
17
+ { shape: { a: Integer, b: Integer } }
18
+ )
19
+ end
20
+
21
+ it 'should return a hash when a pemitted is of Integer shape(deep)' do
22
+ input = { param: { a: { b: 2 } } }
23
+ expect(input).to eq permitted_params(
24
+ input,
25
+ { param: Shape },
26
+ false,
27
+ { shape: { a: { b: Integer } } }
28
+ )
29
+ end
30
+
31
+ it 'should return a hash when a pemitted is of Array shape(deep)' do
32
+ input = { param: { a: { b: [1, 2] } } }
33
+ expect(input).to eq permitted_params(
34
+ input,
35
+ { param: Shape },
36
+ false,
37
+ { shape: { a: { b: Array } } }
38
+ )
39
+ end
40
+
41
+ it 'should return a empty object when a pemitted is shape not defined' do
42
+ input = { param: { a: 1, b: 2 } }
43
+ expect({}).to eq permitted_params(
44
+ input,
45
+ { param: Shape },
46
+ false,
47
+ { shape: { a: Integer } }
48
+ )
49
+ end
50
+ end
@@ -7,140 +7,128 @@ require 'rack/test'
7
7
 
8
8
  include PermitParams
9
9
 
10
- describe 'exceptions' do
10
+ describe 'behaviours' do
11
11
  it 'should raise error when at least one param is invalid' do
12
- input = { param_1: 'a' }
12
+ input = { param: 'a' }
13
13
  expect do
14
- permitted_params(input, { param_1: Integer }, true)
14
+ permitted_params(input, { param: Integer }, true)
15
15
  end.to raise_error(InvalidParameterError, "'a' is not a valid Integer")
16
16
  end
17
17
 
18
18
  it 'should allow all params when no restriction is given' do
19
- input = { param_1: 'a string' }
19
+ input = { param: 'a string' }
20
20
  expect(input).to eq permitted_params(input)
21
21
  end
22
22
 
23
- it 'should remove a string when a pemitted is integer' do
24
- input = { param_1: 'a string' }
23
+ it 'should remove a string when permitted param is integer' do
24
+ input = { param: 'a string' }
25
25
  output = {}
26
- expect(output).to eq permitted_params(input, { param_1: Integer })
26
+ expect(output).to eq permitted_params(input, { param: Integer })
27
27
  end
28
28
 
29
- it 'should return an integer when a pemitted is integer' do
30
- input = { param_1: 1 }
31
- expect(input).to eq permitted_params(input, { param_1: Integer })
29
+ it 'should return an integer when permitted param is integer' do
30
+ input = { param: 1 }
31
+ expect(input).to eq permitted_params(input, { param: Integer })
32
32
  end
33
33
 
34
- it 'should return an integer when a pemitted can be cast into integer' do
35
- input = { param_1: '1' }
36
- output = { param_1: 1 }
37
- expect(output).to eq permitted_params(input, { param_1: Integer })
34
+ it 'should return an integer when permitted param can be cast into integer' do
35
+ input = { param: '1' }
36
+ output = { param: 1 }
37
+ expect(output).to eq permitted_params(input, { param: Integer })
38
38
  end
39
39
 
40
- it 'should return a string when a pemitted is string' do
41
- input = { param_1: 'a string' }
42
- expect(input).to eq permitted_params(input, { param_1: String })
40
+ it 'should return a string when permitted param is string' do
41
+ input = { param: 'a string' }
42
+ expect(input).to eq permitted_params(input, { param: String })
43
43
  end
44
44
 
45
- it 'should return a float when a pemitted is float' do
46
- input = { param_1: 10.0 }
47
- expect(input).to eq permitted_params(input, { param_1: Float })
45
+ it 'should return a float when permitted param is float' do
46
+ input = { param: 10.0 }
47
+ expect(input).to eq permitted_params(input, { param: Float })
48
48
  end
49
49
 
50
- it 'should return a date when a pemitted is date' do
51
- input = { param_1: Date.new }
52
- expect(input).to eq permitted_params(input, { param_1: Date })
50
+ it 'should return a date when permitted param is date' do
51
+ input = { param: Date.new }
52
+ expect(input).to eq permitted_params(input, { param: Date })
53
53
  end
54
54
 
55
- it 'should return a time when a pemitted is time' do
56
- input = { param_1: Time.new }
57
- expect(input).to eq permitted_params(input, { param_1: Time })
55
+ it 'should return a time when permitted param is time' do
56
+ input = { param: Time.new }
57
+ expect(input).to eq permitted_params(input, { param: Time })
58
58
  end
59
59
 
60
- it 'should return a false(boolean) when a pemitted is boolean' do
61
- input = { param_1: 'false' }
62
- output = { param_1: false }
63
- expect(output).to eq permitted_params(input, { param_1: Boolean })
60
+ it 'should return a false(boolean) when permitted param is boolean' do
61
+ input = { param: 'false' }
62
+ output = { param: false }
63
+ expect(output).to eq permitted_params(input, { param: Boolean })
64
64
  end
65
65
 
66
- it 'should return a true(boolean) when a pemitted is boolean' do
67
- input = { param_1: 'true' }
68
- output = { param_1: true }
69
- expect(output).to eq permitted_params(input, { param_1: Boolean })
66
+ it 'should return a true(boolean) when permitted param is boolean' do
67
+ input = { param: 'true' }
68
+ output = { param: true }
69
+ expect(output).to eq permitted_params(input, { param: Boolean })
70
70
  end
71
71
 
72
- it 'should return an array when a pemitted is array' do
73
- input = { param_1: [1, 2] }
74
- expect(input).to eq permitted_params(input, { param_1: Array })
72
+ it 'should return an array when permitted param is array' do
73
+ input = { param: [1, 2] }
74
+ expect(input).to eq permitted_params(input, { param: Array })
75
75
  end
76
76
 
77
- it 'should return an array when a pemitted is array' do
78
- input = { param_1: '1, 2' }
79
- output = { param_1: %w[1 2] }
80
- expect(output).to eq permitted_params(input, { param_1: Array })
77
+ it 'should return an array when permitted param is array' do
78
+ input = { param: '1, 2' }
79
+ output = { param: %w[1 2] }
80
+ expect(output).to eq permitted_params(input, { param: Array })
81
81
  end
82
82
 
83
- it 'should return an array when a pemitted is array' do
84
- input = { param_1: '1; 2' }
85
- output = { param_1: %w[1 2] }
86
- expect(output).to eq permitted_params(input, { param_1: Array }, false, { delimiter: ';' })
83
+ it 'should return an array when permitted param is array' do
84
+ input = { param: '1; 2' }
85
+ output = { param: %w[1 2] }
86
+ expect(output).to eq permitted_params(input, { param: Array }, false, { delimiter: ';' })
87
87
  end
88
88
 
89
- it 'should return a hash when a pemitted is hash' do
90
- input = { param_1: 'a: 1, b: 2' }
91
- output = { param_1: { 'a' => '1', 'b' => '2' } }
92
- expect(output).to eq permitted_params(input, { param_1: Hash })
89
+ it 'should return an empty hash when permitted param is array with wrong delimiter' do
90
+ input = { param: '1; 2' }
91
+ output = {}
92
+ expect(output).to eq permitted_params(input, { param: Array }, false, { delimiter: ',' })
93
93
  end
94
94
 
95
- it 'should return a hash when a pemitted is hash' do
96
- input = { param_1: 'a: 1; b: 2' }
97
- output = { param_1: { 'a' => '1', 'b' => '2' } }
98
- expect(output).to eq permitted_params(input, { param_1: Hash }, false, { delimiter: ';' })
95
+ it 'should return a hash when permitted param is hash' do
96
+ input = { param: 'a: 1, b: 2' }
97
+ output = { param: { 'a' => '1', 'b' => '2' } }
98
+ expect(output).to eq permitted_params(input, { param: Hash })
99
99
  end
100
100
 
101
- it 'should return a hash when a pemitted is hash' do
102
- input = { param_1: { a: 1 } }
103
- expect(input).to eq permitted_params(input, { param_1: Hash })
101
+ it 'should return a hash when permitted param is hash' do
102
+ input = { param: 'a: 1; b: 2' }
103
+ output = { param: { 'a' => '1', 'b' => '2' } }
104
+ expect(output).to eq permitted_params(input, { param: Hash }, false, { delimiter: ';' })
104
105
  end
105
106
 
106
- it 'should return a hash when a pemitted is shape' do
107
- input = { param_1: { a: 1, b: 2 } }
108
- expect(input).to eq permitted_params(
109
- input,
110
- { param_1: Shape },
111
- false,
112
- { shape: { a: Integer, b: Integer } }
113
- )
107
+ it 'should return an empty hash when permitted param is hash with wrong separator' do
108
+ input = { param: 'a: 1; b: 2' }
109
+ output = {}
110
+ expect(output).to eq permitted_params(input, { param: Hash }, false, { separator: '.' })
114
111
  end
115
112
 
116
- it 'should return a hash when a pemitted is shape(deep)' do
117
- input = { param_1: { a: { b: 2 } } }
118
- expect(input).to eq permitted_params(
119
- input,
120
- { param_1: Shape },
121
- false,
122
- { shape: { a: { b: Integer } } }
123
- )
113
+ it 'should return an empty hash when permitted param is hash with wrong delimiter' do
114
+ input = { param: 'a: 1; b: 2' }
115
+ output = {}
116
+ expect(output).to eq permitted_params(input, { param: Hash }, false, { delimiter: ',' })
124
117
  end
125
118
 
126
- it 'should return a empty when a pemitted is shape not defined' do
127
- input = { param_1: { a: 1, b: 2 } }
128
- expect({}).to eq permitted_params(
129
- input,
130
- { param_1: Shape },
131
- false,
132
- { shape: { a: Integer } }
133
- )
119
+ it 'should return a hash when permitted param is hash' do
120
+ input = { param: { a: 1 } }
121
+ expect(input).to eq permitted_params(input, { param: Hash })
134
122
  end
135
123
 
136
- it 'should return a hash when a pemitted is hash' do
137
- input = { param_1: { a: { b: 1 } } }
138
- expect(input).to eq permitted_params(input, { param_1: Hash })
124
+ it 'should return a hash when permitted param is hash' do
125
+ input = { param: { a: { b: 1 } } }
126
+ expect(input).to eq permitted_params(input, { param: Hash })
139
127
  end
140
128
 
141
- it 'should return a hash when a pemitted is hash' do
142
- input = { param_1: { "a": 1 } }
143
- expect(input).to eq permitted_params(input, { param_1: Hash })
129
+ it 'should return a hash when permitted param is hash' do
130
+ input = { param: { "a": 1 } }
131
+ expect(input).to eq permitted_params(input, { param: Hash })
144
132
  end
145
133
 
146
134
  it 'should return a several types for several inputs' do
@@ -167,21 +155,21 @@ describe 'exceptions' do
167
155
  end
168
156
  end
169
157
 
170
- input = { param_1: '1' }
171
- output = { param_1: '1' }
172
- expect(output).to eq permitted_params(input, { param_1: Any })
158
+ input = { param: '1' }
159
+ output = { param: '1' }
160
+ expect(output).to eq permitted_params(input, { param: Any })
173
161
 
174
162
  input = {
175
- param_1: TestClass.new(some_attribute: 'a string')
163
+ param: TestClass.new(some_attribute: 'a string')
176
164
  }
177
- output = permitted_params(input, { param_1: Any })
178
- expect(input[:param_1].some_attribute).to eq output[:param_1].some_attribute
165
+ output = permitted_params(input, { param: Any })
166
+ expect(input[:param].some_attribute).to eq output[:param].some_attribute
179
167
 
180
168
  input = {
181
- param_1: TestClass.new(some_attribute: 1),
182
- param_2: 2
169
+ param: TestClass.new(some_attribute: 1),
170
+ antoher_param: 2
183
171
  }
184
- output = permitted_params(input, { param_1: Any })
185
- expect(input[:param_1].some_attribute).to eq output[:param_1].some_attribute
172
+ output = permitted_params(input, { param: Any })
173
+ expect(input[:param].some_attribute).to eq output[:param].some_attribute
186
174
  end
187
175
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-my-params
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marco Aviles
@@ -10,7 +10,7 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 2021-09-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
- description: A simple params sanitizer(originally created for sinatra)
13
+ description: A simple params sanitizer (originally created for sinatra)
14
14
  email: gdmarav374@gmail.com
15
15
  executables: []
16
16
  extensions: []
@@ -19,6 +19,7 @@ files:
19
19
  - Rakefile
20
20
  - bin/permit_params
21
21
  - lib/permit_params.rb
22
+ - spec/permit_params_shape_spec.rb
22
23
  - spec/permit_params_spec.rb
23
24
  homepage: https://github.com/mhero/sinatra-my-params
24
25
  licenses: []
@@ -38,9 +39,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
38
39
  - !ruby/object:Gem::Version
39
40
  version: '0'
40
41
  requirements: []
41
- rubygems_version: 3.2.3
42
+ rubygems_version: 3.4.19
42
43
  signing_key:
43
44
  specification_version: 3
44
45
  summary: permit_params!
45
46
  test_files:
46
47
  - spec/permit_params_spec.rb
48
+ - spec/permit_params_shape_spec.rb