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 +4 -4
- data/CHANGELOG.md +9 -0
- data/Gemfile.lock +1 -1
- data/lib/rhales/core/view.rb +1 -1
- data/lib/rhales/hydration/hydration_endpoint.rb +18 -2
- data/lib/rhales/utils/json_serializer.rb +29 -0
- data/lib/rhales/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bc4ab2b108b173b1781fb48ada4b2cfba22547d79d0ecc4659838a67c2407363
|
|
4
|
+
data.tar.gz: 59f4e4b981c3166722d3ddebeba27861dc949575719c8eaa53d23b4e6a41f8d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/lib/rhales/core/view.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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).
|
data/lib/rhales/version.rb
CHANGED