view_component 4.11.0 → 4.12.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/docs/CHANGELOG.md +10 -0
- data/lib/view_component/base.rb +44 -9
- data/lib/view_component/collection.rb +14 -11
- data/lib/view_component/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: b2f1a7d34956fc9ae629c0f0e710f4a7335b33ebc99f1c3eccafc41d19713524
|
|
4
|
+
data.tar.gz: efd4bec7d2425da171f89f28fbc1bfaf9f2cba94fd0f2a9cbce674830281938e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d0f5024d419a56b5e178f0cce3d77ac4a1ae2d4d3efc0ac5334e7b55a4dcb6cc7460aa4d60a47bce63918b0a452b53c5425297ef03da2ce074793407acef570
|
|
7
|
+
data.tar.gz: be1d649efec64995de234eb70998001d42cd9b0197bdce00ec5011d68cb2a2784925abf283b3f51c3986a55a446db3ba0c17cedf406c919e31b178ece2be6b51
|
data/docs/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,16 @@ nav_order: 6
|
|
|
10
10
|
|
|
11
11
|
## main
|
|
12
12
|
|
|
13
|
+
## 4.12.0
|
|
14
|
+
|
|
15
|
+
* Fix stale render context on reused component instances. A `ViewComponent::Base` instance memoized its controller, helpers, request, view context, lookup context, view flow, and requested format details on first render via `||=`. Rendering the same instance a second time (intentionally or via aliasing) reused that stale context, which could leak data across requests, sessions, or users. `#render_in` now resets these ivars on every call so each render derives its context from the current view.
|
|
16
|
+
|
|
17
|
+
*Joel Hawksley*
|
|
18
|
+
|
|
19
|
+
* Fix HTML-safety bypass in `around_render`. `ViewComponent::Base#around_render` could return HTML-unsafe strings that bypassed the escaping applied to normal `#call` return values, creating an XSS risk. The vulnerability was amplified in `ViewComponent::Collection#render_in`, which joined per-item results and unconditionally marked the output `html_safe`. HTML-unsafe strings returned from `around_render` are now escaped (with a warning) and `Collection#render_in` now uses `safe_join` so unsafe per-item output is escaped instead of laundered into a `SafeBuffer`.
|
|
20
|
+
|
|
21
|
+
*Joel Hawksley*
|
|
22
|
+
|
|
13
23
|
## 4.11.0
|
|
14
24
|
|
|
15
25
|
* Update `render_in` signature to accept `**_` for compatibility with Rails [#50623](https://github.com/rails/rails/pull/50623).
|
data/lib/view_component/base.rb
CHANGED
|
@@ -106,22 +106,24 @@ module ViewComponent
|
|
|
106
106
|
def render_in(view_context, **_, &block)
|
|
107
107
|
self.class.__vc_compile(raise_errors: true)
|
|
108
108
|
|
|
109
|
+
__vc_reset_render_state!
|
|
110
|
+
|
|
109
111
|
@view_context = view_context
|
|
110
112
|
@old_virtual_path = view_context.instance_variable_get(:@virtual_path)
|
|
111
|
-
self.__vc_original_view_context
|
|
113
|
+
self.__vc_original_view_context = view_context
|
|
112
114
|
|
|
113
115
|
@output_buffer = view_context.output_buffer
|
|
114
116
|
|
|
115
|
-
@lookup_context
|
|
117
|
+
@lookup_context = view_context.lookup_context
|
|
116
118
|
|
|
117
119
|
# For content_for
|
|
118
|
-
@view_flow
|
|
120
|
+
@view_flow = view_context.view_flow
|
|
119
121
|
|
|
120
122
|
# For i18n
|
|
121
123
|
@virtual_path ||= virtual_path
|
|
122
124
|
|
|
123
125
|
# Describes the inferred request constraints (locales, formats, variants)
|
|
124
|
-
@__vc_requested_details
|
|
126
|
+
@__vc_requested_details = @lookup_context.vc_requested_details
|
|
125
127
|
|
|
126
128
|
# For caching, such as #cache_if
|
|
127
129
|
@current_template = nil unless defined?(@current_template)
|
|
@@ -142,10 +144,21 @@ module ViewComponent
|
|
|
142
144
|
value = nil
|
|
143
145
|
|
|
144
146
|
@output_buffer.with_buffer do
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
147
|
+
inner_rendered_template = nil
|
|
148
|
+
around_rendered_template = around_render do
|
|
149
|
+
inner_rendered_template = render_template_for(@__vc_requested_details).to_s
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# If `around_render` returned the same object the block yielded, the inner
|
|
153
|
+
# template's escaping is authoritative and we can trust the result. If the
|
|
154
|
+
# user replaced/wrapped the value, re-check HTML safety to prevent
|
|
155
|
+
# bypassing the escaping applied to normal `#call` return values
|
|
156
|
+
# (GHSA-97jw-64cj-jc58).
|
|
157
|
+
rendered_template = if around_rendered_template.equal?(inner_rendered_template)
|
|
158
|
+
around_rendered_template
|
|
159
|
+
else
|
|
160
|
+
__vc_safe_around_render_output(around_rendered_template)
|
|
161
|
+
end
|
|
149
162
|
|
|
150
163
|
# Avoid allocating new string when output_preamble and output_postamble are blank
|
|
151
164
|
value = if output_preamble.blank? && output_postamble.blank?
|
|
@@ -255,7 +268,7 @@ module ViewComponent
|
|
|
255
268
|
# @private
|
|
256
269
|
def render(options = {}, args = {}, &block)
|
|
257
270
|
if options.respond_to?(:set_original_view_context)
|
|
258
|
-
options.set_original_view_context(
|
|
271
|
+
options.set_original_view_context(__vc_original_view_context)
|
|
259
272
|
|
|
260
273
|
# We assume options is a component, so there's no need to evaluate the
|
|
261
274
|
# block in the view context as we do below.
|
|
@@ -444,6 +457,28 @@ module ViewComponent
|
|
|
444
457
|
end
|
|
445
458
|
end
|
|
446
459
|
|
|
460
|
+
def __vc_safe_around_render_output(output)
|
|
461
|
+
__vc_maybe_escape_html(output) do
|
|
462
|
+
Kernel.warn("WARNING: The #{self.class} component's around_render returned an HTML-unsafe string. The output will be automatically escaped, but you may want to investigate.")
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Resets every render-scoped instance variable derived from the calling view
|
|
467
|
+
# context so a reused instance cannot leak controller/helper/request/format
|
|
468
|
+
# state from a previous render. Slot state (`@__vc_set_slots`,
|
|
469
|
+
# `@__vc_content_set_by_with_content`) is intentionally preserved because it
|
|
470
|
+
# is populated by callers _before_ `render_in` runs (e.g. via `with_*`
|
|
471
|
+
# slot setters or `with_content`).
|
|
472
|
+
def __vc_reset_render_state!
|
|
473
|
+
%i[
|
|
474
|
+
@__vc_controller
|
|
475
|
+
@__vc_helpers
|
|
476
|
+
@__vc_request
|
|
477
|
+
].each do |ivar|
|
|
478
|
+
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
|
|
447
482
|
# Configuration for generators.
|
|
448
483
|
#
|
|
449
484
|
# All options under this namespace default to `false` unless otherwise
|
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "action_view/renderer/collection_renderer"
|
|
4
|
+
require "action_view/helpers/output_safety_helper"
|
|
4
5
|
|
|
5
6
|
module ViewComponent
|
|
6
7
|
class Collection
|
|
7
8
|
include Enumerable
|
|
9
|
+
include ActionView::Helpers::OutputSafetyHelper
|
|
8
10
|
|
|
9
11
|
attr_reader :component
|
|
10
12
|
|
|
11
13
|
delegate :size, to: :@collection
|
|
12
14
|
|
|
13
15
|
def render_in(view_context, **_, &block)
|
|
14
|
-
components.map do |component|
|
|
16
|
+
rendered = components.map do |component|
|
|
15
17
|
component.render_in(view_context, &block)
|
|
16
|
-
end
|
|
18
|
+
end
|
|
19
|
+
safe_join(rendered, rendered_spacer(view_context))
|
|
17
20
|
end
|
|
18
21
|
|
|
19
22
|
def each(&block)
|
|
@@ -30,15 +33,15 @@ module ViewComponent
|
|
|
30
33
|
|
|
31
34
|
private
|
|
32
35
|
|
|
36
|
+
# Always rebuild child component instances per render to avoid leaking
|
|
37
|
+
# request-scoped state from a previous render into a later one (GHSA).
|
|
33
38
|
def components
|
|
34
|
-
return @components if defined? @components
|
|
35
|
-
|
|
36
39
|
iterator = ActionView::PartialIteration.new(@collection.size)
|
|
37
40
|
|
|
38
41
|
component.__vc_validate_collection_parameter!(validate_default: true)
|
|
39
42
|
|
|
40
|
-
@
|
|
41
|
-
component.new(**component_options(item, iterator)).tap do |
|
|
43
|
+
@collection.map do |item|
|
|
44
|
+
component.new(**component_options(item, iterator)).tap do |_|
|
|
42
45
|
iterator.iterate!
|
|
43
46
|
end
|
|
44
47
|
end
|
|
@@ -67,12 +70,12 @@ module ViewComponent
|
|
|
67
70
|
@options.merge(item_options)
|
|
68
71
|
end
|
|
69
72
|
|
|
73
|
+
# Render the spacer through a fresh `dup` so a collection rendered multiple
|
|
74
|
+
# times always gets a clean spacer instance.
|
|
70
75
|
def rendered_spacer(view_context)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
""
|
|
75
|
-
end
|
|
76
|
+
return "" unless @spacer_component
|
|
77
|
+
|
|
78
|
+
@spacer_component.dup.render_in(view_context)
|
|
76
79
|
end
|
|
77
80
|
end
|
|
78
81
|
end
|