rhales 0.6.2 → 0.7.0

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: 249890f733fb9b88bbabe22bcc327aa5d0422c00ea595e4b67751e335e790feb
4
- data.tar.gz: 69259f0ab5cc091cc2c4fab33e3eee13859e28ef789e522119e6702f695155ab
3
+ metadata.gz: bc4ab2b108b173b1781fb48ada4b2cfba22547d79d0ecc4659838a67c2407363
4
+ data.tar.gz: 59f4e4b981c3166722d3ddebeba27861dc949575719c8eaa53d23b4e6a41f8d2
5
5
  SHA512:
6
- metadata.gz: c424d4d348c48686ddb60fe068d2839627c552d94a03a507b9cf54f1d39d11dc4d18aaefccc8731ec0aa8fb79d72ef18ea838460d0e36f5d69b5575d7d15ac3d
7
- data.tar.gz: 65139df9054a39981ca7f6fd51818544aca039847d4a1d60521e879c2d542f3ca1f2ca9bbeb030154cc735404882d5ca0ede2fcabe64ef55a8194c622518c963
6
+ metadata.gz: 06547d4e90656b20b5dd8dee5f6b468ea5ff63fc1f76becdca3b48889af7f62a42793acca89ced4f32f91017eb026f18862d772ffa1c606582a4aa0ad8fbb490
7
+ data.tar.gz: 1d5eb58558b5b45c3c24c95f2ee0f26ba171e6c11553e3b55d41aa598c41f2f53573b7e05c36abd52865d7b884aa571a076698183a4a1b135f31ec1a29b3319c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.7.0] - 2026-06-21
11
+
12
+ ### Security
13
+ - Validate JSONP callback names against a JS identifier / dotted-path pattern to
14
+ block reflected XSS; invalid names raise `ArgumentError` (`HydrationEndpoint#render_jsonp`).
15
+ - Escape `<`, `>`, `&`, U+2028, and U+2029 in hydration JSON so payloads like
16
+ `</script>` can't break out of the script context (`JSONSerializer.dump_html_safe`,
17
+ used by `View` and `HydrationEndpoint`).
18
+
10
19
  ## [0.6.2] - 2026-05-25
11
20
 
12
21
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rhales (0.6.2)
4
+ rhales (0.7.0)
5
5
  json_schemer (~> 2)
6
6
  logger
7
7
  tilt (~> 2)
@@ -390,7 +390,7 @@ module Rhales
390
390
  # Create JSON script tag with optional reflection attributes
391
391
  json_attrs = reflection_enabled? ? " data-window=\"#{window_attr}\"" : ''
392
392
  json_script = <<~HTML.strip
393
- <script#{nonce_attr} id="#{unique_id}" type="application/json"#{json_attrs}>#{JSONSerializer.dump(data)}</script>
393
+ <script#{nonce_attr} id="#{unique_id}" type="application/json"#{json_attrs}>#{JSONSerializer.dump_html_safe(data)}</script>
394
394
  HTML
395
395
 
396
396
  # Create hydration script with optional reflection attributes
@@ -42,6 +42,14 @@ module Rhales
42
42
  # module_response = endpoint.render_module('template_name')
43
43
  # ```
44
44
  class HydrationEndpoint
45
+ # Valid JSONP callback names: a JS identifier or dotted member path
46
+ # (e.g. "handleData", "app.callbacks.handleData"). Each dotted segment must
47
+ # be its own valid identifier, so malformed paths like "foo.", "foo..bar"
48
+ # or "foo.1bar" are rejected rather than producing unusable JSONP. Anything
49
+ # outside this set could break out of the callback invocation and inject
50
+ # script.
51
+ CALLBACK_NAME_PATTERN = /\A[a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z_$][a-zA-Z0-9_$]*)*\z/
52
+
45
53
  def initialize(config, context = nil)
46
54
  @config = config
47
55
  @context = context
@@ -93,18 +101,26 @@ module Rhales
93
101
  merged_data = process_template_data(template_name, additional_context)
94
102
 
95
103
  {
96
- content: "export default #{JSONSerializer.dump(merged_data)};",
104
+ content: "export default #{JSONSerializer.dump_html_safe(merged_data)};",
97
105
  content_type: 'text/javascript',
98
106
  headers: module_headers(merged_data)
99
107
  }
100
108
  end
101
109
 
102
110
  # Render JSONP response with callback
111
+ #
112
+ # The callback name is reflected directly into the executable response
113
+ # body, so it must be validated before use. Without validation a caller
114
+ # supplying something like "alert(1)//" would inject arbitrary JavaScript.
103
115
  def render_jsonp(template_name, callback_name, additional_context = {})
116
+ unless callback_name.is_a?(String) && callback_name.match?(CALLBACK_NAME_PATTERN)
117
+ raise ArgumentError, "Invalid callback: #{callback_name.inspect}"
118
+ end
119
+
104
120
  merged_data = process_template_data(template_name, additional_context)
105
121
 
106
122
  {
107
- content: "#{callback_name}(#{JSONSerializer.dump(merged_data)});",
123
+ content: "#{callback_name}(#{JSONSerializer.dump_html_safe(merged_data)});",
108
124
  content_type: 'application/javascript',
109
125
  headers: jsonp_headers(merged_data),
110
126
  }
@@ -32,6 +32,19 @@ module Rhales
32
32
  # # => :oj (if available) or :json (stdlib)
33
33
  #
34
34
  module JSONSerializer
35
+ # Characters that can terminate an HTML <script> element or, in legacy
36
+ # JavaScript engines, a string literal. They are escaped as \uXXXX, which
37
+ # is equivalent JSON/JS and round-trips identically through any parser.
38
+ HTML_ESCAPE = {
39
+ '<' => '\u003c',
40
+ '>' => '\u003e',
41
+ '&' => '\u0026',
42
+ "\u2028" => '\u2028',
43
+ "\u2029" => '\u2029',
44
+ }.freeze
45
+
46
+ HTML_ESCAPE_PATTERN = /[<>&

]/
47
+
35
48
  class << self
36
49
  # Serialize Ruby object to JSON string
37
50
  #
@@ -44,6 +57,22 @@ module Rhales
44
57
  @json_dumper.call(obj)
45
58
  end
46
59
 
60
+ # Serialize Ruby object to a JSON string safe to embed in an HTML
61
+ # <script> element or a JavaScript resource body.
62
+ #
63
+ # Standard JSON generation does not escape <, >, & or the U+2028/U+2029
64
+ # line separators, so a value containing "</script>" would break out of
65
+ # the surrounding script context and allow markup/JavaScript injection
66
+ # (XSS). This method escapes those characters as \uXXXX. The result is
67
+ # still valid JSON and parses back to the identical value.
68
+ #
69
+ # @param obj [Object] Ruby object to serialize
70
+ # @return [String] HTML/JS-context-safe JSON string
71
+ # @raise [TypeError] if object contains non-serializable types
72
+ def dump_html_safe(obj)
73
+ dump(obj).gsub(HTML_ESCAPE_PATTERN, HTML_ESCAPE)
74
+ end
75
+
47
76
  # Serialize Ruby object to pretty-printed JSON string
48
77
  #
49
78
  # Uses the serializer backend determined at load time (Oj or stdlib JSON).
@@ -5,6 +5,6 @@
5
5
  module Rhales
6
6
  # Version information for the RSFC gem
7
7
  unless defined?(Rhales::VERSION)
8
- VERSION = '0.6.2'
8
+ VERSION = '0.7.0'
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhales
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - delano