sparkql 1.3.4 → 1.3.6

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: 995514ae40305cedb2116dafc6c98b8efb534ae3d2f31c9d48e86421e3f5c10c
4
- data.tar.gz: 6162d29fb4520941edb2b53742d966fdf373418b2745f6fd1d8c1fa79432b896
3
+ metadata.gz: 6c51d91b09c229564b83503e11d3ca453c91f5c280307effd1120d6a33b74010
4
+ data.tar.gz: ea90a995aa3dbd06ae7f187437c24abb81590ff396e837ec45e531ea25408f69
5
5
  SHA512:
6
- metadata.gz: c39c06894d6a7b298d4fafb3c578e6d203543c77c68ad0cf8acb9ec943c00c656e38529a7e6b2afe4cc3177b5a59b078c7ce4c193a08dea0f575bd62ee0495d7
7
- data.tar.gz: fbbba23b099146c85f3c15fb82ea3bbc93fe989b156752a80137a2212b2ed64f4cf50ecb99bf06b59914f1e1f8996a65155c9cde11c025702e6b8504b083cc93
6
+ metadata.gz: c10a6629cc9606cdb49de2f60c64e6d0a38879c7abf4118e91fb23070eea06efa81b951a4f76daf185f5904efcbaa1b2ef4edb09a6124f24833e77865ecdd295
7
+ data.tar.gz: 9e073aa1c7963578f2ecf8e0a65230e7210c329a365a57e6b3fd7d4d0c7aa60e281b186e3c507ffe8f59a91fbefd16dbc9e99a9d97e8b603f4da6b1bb859c0e4
data/CHANGELOG.md CHANGED
@@ -1,4 +1,10 @@
1
- v1.3.4, 20265-01-20
1
+ v1.3.6, 2026-06-23
2
+ * [BUGFIX] Parser crash when days()/weekdays() exceed max offset
3
+
4
+ v1.3.5, 2026-04-30
5
+ * [IMPROVEMENT] add date_date() function
6
+
7
+ v1.3.4, 2026-01-20
2
8
  * [BUGFIX] Validate limit for days and weekdays
3
9
 
4
10
  v1.3.3, 2025-08-12
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.4
1
+ 1.3.6
@@ -651,6 +651,10 @@ module Sparkql
651
651
  }
652
652
  end
653
653
 
654
+ def date_date(value)
655
+ date_datetime(value)
656
+ end
657
+
654
658
  def time_datetime(datetime)
655
659
  {
656
660
  type: :time,
@@ -171,6 +171,16 @@ module Sparkql::ParserTools
171
171
  end
172
172
 
173
173
  def tokenize_multiple(lit1, lit2)
174
+ if lit1.nil? || lit2.nil?
175
+ return nil if errors?
176
+
177
+ tokenizer_error(token: @lexer.last_field,
178
+ message: 'Invalid value in field list.',
179
+ status: :fatal,
180
+ syntax: true)
181
+ return nil
182
+ end
183
+
174
184
  final_type = lit1[:type]
175
185
  if lit1[:type] != lit2[:type]
176
186
  final_type = coercible_types(lit1[:type], lit2[:type])
@@ -224,15 +234,17 @@ module Sparkql::ParserTools
224
234
  resolver = function_resolver(name, args)
225
235
  resolver.validate
226
236
  if resolver.errors?
227
- tokenizer_error(token: @lexer.last_field,
228
- message: "Error parsing function #{resolver.errors.join(',')}",
229
- status: :fatal,
230
- syntax: true)
231
- nil
232
- else
233
- result = resolver.call
234
- result.nil? ? result : result.merge(condition: "#{name}(#{condition_list.join(',')})")
237
+ report_function_resolver_errors(resolver)
238
+ return nil
235
239
  end
240
+
241
+ result = resolver.call
242
+ if result.nil?
243
+ report_function_resolver_errors(resolver) if resolver.errors?
244
+ return nil
245
+ end
246
+
247
+ result.merge(condition: "#{name}(#{condition_list.join(',')})")
236
248
  end
237
249
 
238
250
  def tokenize_arithmetic(lhs, operator, rhs)
@@ -448,4 +460,16 @@ module Sparkql::ParserTools
448
460
  def offset
449
461
  @offset ||= current_timestamp.strftime('%:z')
450
462
  end
463
+
464
+ private
465
+
466
+ def report_function_resolver_errors(resolver)
467
+ resolver.errors.each do |error|
468
+ tokenizer_error(token: error.token || @lexer.last_field,
469
+ message: error.message,
470
+ status: error.status,
471
+ syntax: error.syntax?,
472
+ constraint: error.constraint?)
473
+ end
474
+ end
451
475
  end
@@ -1232,6 +1232,18 @@ class ParserTest < Test::Unit::TestCase
1232
1232
  assert_equal 0, expressions.last[:block_group]
1233
1233
  end
1234
1234
 
1235
+ test 'oversized days() in Bt range returns parser error instead of raising' do
1236
+ [
1237
+ 'OriginalEntryTimestamp Bt days(0),days(365001)',
1238
+ 'OpenHouses Bt days(0),days(9223372036854775807)'
1239
+ ].each do |filter|
1240
+ @parser = Parser.new
1241
+ assert_nothing_raised { @parser.parse(filter) }
1242
+ assert @parser.fatal_errors?, "Expected fatal error for #{filter}"
1243
+ assert_match(/max offset/i, @parser.errors.first.message)
1244
+ end
1245
+ end
1246
+
1235
1247
  private
1236
1248
 
1237
1249
  def parser_errors(filter)
@@ -0,0 +1,150 @@
1
+ require 'test_helper'
2
+
3
+ # Security regression tests covering SQL injection attack vectors identified
4
+ # during audit. The parser is responsible for rejecting malformed input and
5
+ # producing type-safe output. Consumers of parser output must still use
6
+ # parameterized queries and quote field identifiers — see comments below.
7
+ class SecurityTest < Test::Unit::TestCase
8
+ include Sparkql
9
+
10
+ def setup
11
+ @parser = Parser.new
12
+ end
13
+
14
+ # --- String value injection ---
15
+
16
+ test 'rejects unclosed string literals' do
17
+ assert_parse_error "City Eq 'Fargo"
18
+ assert_parse_error "City Eq Fargo'"
19
+ end
20
+
21
+ test 'rejects double-quote SQL injection in string values' do
22
+ # SparkQL only recognises \' as an escape sequence inside strings.
23
+ # A bare '' does not escape the quote — the lexer matches 'test' and
24
+ # the leftover 'injection' token causes a parse error.
25
+ assert_parse_error "City Eq 'test''injection'"
26
+ end
27
+
28
+ test 'string with backslash-escaped quote parses and escapes safely' do
29
+ # \' is the valid SparkQL escape for a literal apostrophe. :value retains
30
+ # outer quotes; character_escape strips them and unescapes \' to '.
31
+ # The resulting value contains a single quote — consumers MUST use bind
32
+ # parameters, not string interpolation, when building SQL from this value.
33
+ expressions = @parser.parse("City Eq 'O\\'Brien'")
34
+ assert !@parser.errors?, @parser.errors.inspect
35
+ assert_equal "O'Brien", @parser.character_escape(expressions.first[:value])
36
+ end
37
+
38
+ test 'sql payload inside valid sparkql string is accepted but value is raw' do
39
+ # The lexer correctly accepts 'val\'; DROP TABLE t; --' as a character
40
+ # literal (\' is a valid escape sequence). character_escape returns the raw
41
+ # string including the injected SQL — safe only with parameterized queries.
42
+ expressions = @parser.parse("City Eq 'val\\'; DROP TABLE t; --'")
43
+ assert !@parser.errors?, @parser.errors.inspect
44
+ assert_equal "val'; DROP TABLE t; --", @parser.character_escape(expressions.first[:value])
45
+ end
46
+
47
+ test 'plain string :value retains outer quotes before character_escape' do
48
+ expressions = @parser.parse("City Eq 'Fargo'")
49
+ assert !@parser.errors?, @parser.errors.inspect
50
+ assert_equal "'Fargo'", expressions.first[:value]
51
+ assert_equal :character, expressions.first[:type]
52
+ assert_equal 'Fargo', @parser.character_escape(expressions.first[:value])
53
+ end
54
+
55
+ # --- Numeric/decimal injection ---
56
+
57
+ test 'rejects sql keywords appended after integer token' do
58
+ assert_parse_error "Price Eq 100; DROP TABLE listings"
59
+ assert_parse_error "Price Eq 100 UNION SELECT"
60
+ end
61
+
62
+ test 'integer values are coerced to integers preventing non-numeric injection' do
63
+ expressions = @parser.parse("Price Eq 100")
64
+ assert !@parser.errors?, @parser.errors.inspect
65
+ assert_equal '100', expressions.first[:value]
66
+ assert_equal :integer, expressions.first[:type]
67
+ end
68
+
69
+ test 'decimal values are stored as strings preventing non-numeric injection' do
70
+ expressions = @parser.parse("Price Eq 100.50")
71
+ assert !@parser.errors?, @parser.errors.inspect
72
+ assert_equal '100.50', expressions.first[:value]
73
+ assert_equal :decimal, expressions.first[:type]
74
+ end
75
+
76
+ # --- Operator injection ---
77
+
78
+ test 'rejects sql operators that are not in the sparkql whitelist' do
79
+ assert_parse_error "City LIKE '%Fargo%'"
80
+ assert_parse_error "Price > 100"
81
+ assert_parse_error "Price < 100"
82
+ assert_parse_error "Price != 100"
83
+ end
84
+
85
+ test 'rejects sql keywords used as conjunctions' do
86
+ # OR/AND lowercased is valid SparkQL, but 1=1 is not a valid expression.
87
+ # Uppercase SQL keywords like UNION are rejected entirely.
88
+ assert_parse_error "City Eq 'Fargo' UNION SELECT City FROM listings"
89
+ assert_parse_error "City Eq 'Fargo' OR 1=1"
90
+ assert_parse_error "City Eq 'Fargo' AND 1=1"
91
+ end
92
+
93
+ # --- Function name injection ---
94
+
95
+ test 'rejects function names not in the supported functions whitelist' do
96
+ assert_parse_error "City Eq unknownfn('Fargo')"
97
+ assert_parse_error "City Eq exec('xp_cmdshell')"
98
+ assert_parse_error "City Eq sleep(10)"
99
+ end
100
+
101
+ # --- Custom field name handling ---
102
+
103
+ test 'custom field regex rejects names starting with dollar sign or period' do
104
+ assert_parse_error '"$BadGroup"."Field" Eq 10'
105
+ assert_parse_error '"Group"."$BadField" Eq 10'
106
+ assert_parse_error '"Group".".BadField" Eq 10'
107
+ assert_parse_error '"Group.Bad"."Field" Eq 10'
108
+ assert_parse_error '"Group"."Field.Sub" Eq 10'
109
+ assert_parse_error '""."" Eq 10'
110
+ end
111
+
112
+ test 'valid custom field names parse and field is returned verbatim' do
113
+ filter = '"General Property Description"."Taxes" Lt 500.0'
114
+ expressions = @parser.parse(filter)
115
+ assert !@parser.errors?, @parser.errors.inspect
116
+ assert expressions.first[:custom_field]
117
+ # The field value is returned as-is with its surrounding double-quotes.
118
+ # Consumers MUST validate this against allowed metadata and/or quote it
119
+ # as a SQL identifier before using it in a query — never interpolate directly.
120
+ assert_equal '"General Property Description"."Taxes"', expressions.first[:field]
121
+ end
122
+
123
+ test 'custom field name containing sql-significant characters is returned verbatim' do
124
+ # SparkQL allows characters like ; and ' inside double-quoted field names.
125
+ # The CUSTOM_FIELD regex does not strip these — that is the consumer's job.
126
+ filter = %("It's a field"."Value" Eq 10)
127
+ expressions = @parser.parse(filter)
128
+ assert !@parser.errors?, @parser.errors.inspect
129
+ assert expressions.first[:custom_field]
130
+ assert expressions.first[:field].include?("It's a field"), \
131
+ "Expected field to contain the group name, got: #{expressions.first[:field].inspect}"
132
+ end
133
+
134
+ # --- Date/datetime injection ---
135
+
136
+ test 'valid dates parse to their string representation' do
137
+ expressions = @parser.parse("DateField Eq 2023-06-15")
138
+ assert !@parser.errors?, @parser.errors.inspect
139
+ assert_equal '2023-06-15', expressions.first[:value]
140
+ assert_equal :date, expressions.first[:type]
141
+ end
142
+
143
+ private
144
+
145
+ def assert_parse_error(filter)
146
+ parser = Parser.new
147
+ parser.parse(filter)
148
+ assert parser.errors?, "Expected parse error for: #{filter.inspect}"
149
+ end
150
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sparkql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.4
4
+ version: 1.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wade McEwen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-20 00:00:00.000000000 Z
11
+ date: 2026-06-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: georuby
@@ -155,6 +155,7 @@ files:
155
155
  - test/unit/lexer_test.rb
156
156
  - test/unit/parser_compatability_test.rb
157
157
  - test/unit/parser_test.rb
158
+ - test/unit/security_test.rb
158
159
  homepage: ''
159
160
  licenses:
160
161
  - Apache 2.0