sparkql 1.3.4 → 1.3.5
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 +4 -4
- data/CHANGELOG.md +4 -1
- data/VERSION +1 -1
- data/lib/sparkql/function_resolver.rb +4 -0
- data/test/unit/security_test.rb +150 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee4b94747c3d671e7ceac9675caaebaf3d8661a4a9316327719e7221f439b3bc
|
|
4
|
+
data.tar.gz: '09a7eddd5cd21a23d2083c8312f0a1bec48a0f21938c0871538cdb08ef52d1d3'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 67b098969f33bf5eaa19277d3d202687c8ceddf4183d4f54cb703cdb7b5ed3aff934a201a3e238226a27bfcecce9b2c58e31e2ce64f76fa0f03babf23b980291
|
|
7
|
+
data.tar.gz: 42db5ca0692fca37b627562870641af084f539c64909fb8858e8bc872d4371d365621fe502cbba8289dbd0ea47e13cdc80df24c83172ebc3d88a92ec1291e64c
|
data/CHANGELOG.md
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.3.
|
|
1
|
+
1.3.5
|
|
@@ -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
|
+
version: 1.3.5
|
|
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-
|
|
11
|
+
date: 2026-05-14 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
|