arel-extensions 9.0.0 → 9.0.1

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: 2cac2b9e201296fa2c9e058f38cd0acf4835b134f2f98bb74f245af78b58f02c
4
- data.tar.gz: 8b4be319b4037c1dfb2c54d6d9314a6add237ac60ec1fe2e6e1f0d396d67d1bb
3
+ metadata.gz: 99efadd34c12e304c5204c9dc2717bad533bfb20c9c280a036a8abc3110ab34a
4
+ data.tar.gz: 66c064d703b2531c7676e3a91bef9f0dd813355e24108c7966770ea1fba298ff
5
5
  SHA512:
6
- metadata.gz: 36818826669a7004657c3362908fc8c11bb683fabbf93183dde33cb4a933ce3e5bd41da637cfd2c48906fab72d8ed22992802aa44daa69d7919cc3ccc62faffe
7
- data.tar.gz: 17ae44d0099f00b6193f3e94aaa50830556e191d728bc3580b4164fae8f4b6b9f808cea2d3925ffe73cd5a30ea5501be6d393b428e23b01d6b4ee63dfd9d5d32
6
+ metadata.gz: 043be028082fea8d31c8b427db8a703b7f4a2f1916879e77455f05847086964840b4cae0886e73930ace056dc9e86bcb7c375c4794a64f5ac5a85c19d5f9b61b
7
+ data.tar.gz: 89d8e35f5503774cc499c93fa0cc9ea427dec2cecc27cd6ee01f5d52f9f66f9c278078c80c865d7fc4bac8362e9c860f7fe33b1a90b572c4823621cfe6be9ba3
data/CHANGELOG.md CHANGED
@@ -1,4 +1,27 @@
1
- ## [9.0.0] - Unreleased
1
+ ## [9.0.1] - 2026-08-30
2
+
3
+ ### Security
4
+ - Fixed a SQL injection in the PostgreSQL visitor's JSON path handling
5
+ ([GHSA-75hc-9q9v-9cv2], CWE-89, high). `key`/`dig` path segments were
6
+ interpolated straight into a `#>'{...}'` array literal, so a segment
7
+ containing `}'` could close the literal and have the rest of it executed as
8
+ SQL. Most reachable through activerecord-filter, where a filter key like
9
+ `"metadata.<payload>"` on a json/jsonb column puts request input into `dig`.
10
+ Segments are now emitted as a quoted `#> array[...]`, which PostgreSQL folds
11
+ back to the same `text[]` constant (existing expression indexes still match).
12
+ Reported by [@saidM](https://github.com/saidM).
13
+ - `cast_as` now validates the type name and raises `ArgumentError` unless it
14
+ looks like a type identifier. Not reachable from activerecord-filter, but it
15
+ was the same class of raw interpolation.
16
+
17
+ ### Changed
18
+ - A path segment is now always a single segment: `key('a,b')` emits
19
+ `array['a,b']`, where the old raw `'{a,b}'` literal let PostgreSQL split it
20
+ on the comma into two segments. Use `dig('a', 'b')` for multi-segment paths.
21
+
22
+ [GHSA-75hc-9q9v-9cv2]: https://github.com/malomalo/arel-extensions/security/advisories/GHSA-75hc-9q9v-9cv2
23
+
24
+ ## [9.0.0] - 2026-08-27
2
25
 
3
26
  ### Changed
4
27
  - Switched to independent Semantic Versioning. Prior releases tracked the Rails
data/README.md CHANGED
@@ -63,14 +63,44 @@ tags.excludes(other) # NOT (tags @> other)
63
63
  ```ruby
64
64
  data = User.arel_table[:data]
65
65
 
66
- data.key('name') # data #>'{name}' (aliases: data['name'], data.index('name'))
67
- data.dig('address', 'zip') # data #>'{address,zip}'
66
+ # Keys in a Object
67
+ data['name'] # data #> array['name']
68
+ data.key('name') # data #> array['name']
69
+ data.index('name') # data #> array['name']
70
+
71
+ # Integer index in a Array
72
+ data[0] # data #> array['0']
73
+ data.key(0) # data #> array['0']
74
+ data.index(0) # data #> array['0']
75
+
76
+ # Dig and other operators
77
+ data.dig('address', 'zip') # data #> array['address','zip']
78
+ data.dig('tags', -1) # data #> array['tags','-1']
68
79
  data.has_key('name') # data ? 'name'
69
80
  data.has_keys('a', 'b') # data ?& array['a','b']
70
81
  data.has_any_key('a', 'b') # data ?| array['a','b']
71
- data.cast_as('int') # (data)::int
72
82
  ```
73
83
 
84
+ A segment may be an integer, which PostgreSQL reads as an array index (negative
85
+ counts from the end); out of range yields `NULL`.
86
+
87
+ Path segments are quoted, so they are safe to build from untrusted input.
88
+ PostgreSQL folds the array back to `'{address,zip}'::text[]`, so expression
89
+ indexes written against the literal form still match.
90
+
91
+ ### Casting
92
+
93
+ ```ruby
94
+ User.arel_table[:id].cast_as('text') # (users.id)::text
95
+ User.arel_table[:created_at].cast_as('date') # (users.created_at)::date
96
+ data.dig('age').cast_as('int') # (data #> array['age'])::int
97
+ ```
98
+
99
+ A type name can't be quoted or bound, so `cast_as` accepts only something that
100
+ looks like one; optionally schema qualified, with a modifier. For example:
101
+ `text`, `varchar(255)`, `numeric(10,2)`, `timestamp(6) with time zone`,
102
+ `int[]`, `public.geometry`. An `ArgumentError` will be raised otherwise.
103
+
74
104
  ### Full-text search
75
105
 
76
106
  ```ruby
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Arel
4
4
  module Extensions
5
- VERSION = '9.0.0'
5
+ VERSION = '9.0.1'
6
6
  end
7
7
  end
@@ -39,24 +39,24 @@ module Arel
39
39
  collector
40
40
  end
41
41
 
42
- def visit_Arel_Attributes_Key(o, collector, last_key = true)
43
- if o.relation.is_a?(Arel::Attributes::Key)
44
- visit_Arel_Attributes_Key(o.relation, collector, false)
45
- if last_key
46
- collector << o.name.to_s
47
- collector << "}'"
48
- else
49
- collector << o.name.to_s
50
- collector << ","
51
- end
52
- else
53
- visit(o.relation, collector)
54
- collector << "\#>'{" << o.name.to_s
55
- collector << (last_key ? "}'" : ",")
42
+ # Path segments are emitted as a quoted `array[...]` rather than
43
+ # interpolated into a `'{...}'` array literal, so a segment can never
44
+ # break out of the path and inject SQL (GHSA-75hc-9q9v-9cv2). PostgreSQL
45
+ # const-folds the array back to `'{a,b}'::text[]`, so expression indexes
46
+ # on the literal form still match.
47
+ def visit_Arel_Attributes_Key(o, collector)
48
+ keys = []
49
+ node = o
50
+ while node.is_a?(Arel::Attributes::Key)
51
+ keys.unshift(quote(node.name.to_s))
52
+ node = node.relation
56
53
  end
54
+
55
+ visit(node, collector)
56
+ collector << " #> array[" << keys.join(',') << "]"
57
57
  collector
58
58
  end
59
-
59
+
60
60
  def visit_Arel_Nodes_HasKey(o, collector)
61
61
  right = o.right
62
62
 
@@ -84,10 +84,24 @@ module Arel
84
84
  collector
85
85
  end
86
86
 
87
+ # A type name can't be bound or quoted, so only allow something that
88
+ # actually looks like one -- optionally schema qualified, with a modifier
89
+ # and/or array suffix. Keeps user input from reaching the SQL as-is.
90
+ CAST_TYPE = /\A
91
+ [a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)? # type, optionally schema qualified
92
+ (\(\d+(\s*,\s*\d+)?\))? # "varchar(255)", "numeric(10,2)"
93
+ (\ [a-z]+)* # "timestamp with time zone"
94
+ (\(\d+(\s*,\s*\d+)?\))? # "character varying(255)"
95
+ (\[\])* # "int[]"
96
+ \z/xi
97
+
87
98
  def visit_Arel_Attributes_Cast(o, collector)
99
+ type = o.name.to_s
100
+ raise ArgumentError, "invalid cast type: #{type.inspect}" unless CAST_TYPE.match?(type)
101
+
88
102
  collector << "("
89
103
  visit(o.relation, collector)
90
- collector << ")::#{o.name}"
104
+ collector << ")::#{type}"
91
105
  collector
92
106
  end
93
107
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arel-extensions
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.0
4
+ version: 9.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy