universal_renderer 0.5.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/README.md +410 -178
- data/lib/generators/universal_renderer/install_generator.rb +61 -3
- data/lib/generators/universal_renderer/templates/initializer.rb +8 -5
- data/lib/generators/universal_renderer/templates/ssr/config.ts +40 -0
- data/lib/generators/universal_renderer/templates/ssr/dev.ts +9 -0
- data/lib/generators/universal_renderer/templates/ssr/globals.ts +141 -0
- data/lib/generators/universal_renderer/templates/ssr/server.ts +24 -0
- data/lib/generators/universal_renderer/templates/ssr.rake +27 -0
- data/lib/generators/universal_renderer/templates/vite.config.ssr.mts +13 -0
- data/lib/generators/universal_renderer/templates/web +94 -0
- data/lib/universal_renderer/client/base.rb +99 -44
- data/lib/universal_renderer/client/stream/error_logger.rb +25 -3
- data/lib/universal_renderer/client/stream/execution.rb +27 -1
- data/lib/universal_renderer/client/stream/setup.rb +6 -1
- data/lib/universal_renderer/client/stream.rb +49 -12
- data/lib/universal_renderer/configuration.rb +56 -6
- data/lib/universal_renderer/engine.rb +9 -2
- data/lib/universal_renderer/instrumentation.rb +47 -0
- data/lib/universal_renderer/renderable.rb +220 -86
- data/lib/universal_renderer/ssr/helpers.rb +82 -57
- data/lib/universal_renderer/ssr/response.rb +6 -1
- data/lib/universal_renderer/ssr/scrubber.rb +119 -30
- data/lib/universal_renderer/version.rb +1 -1
- data/lib/universal_renderer.rb +1 -0
- metadata +9 -1
|
@@ -1,61 +1,150 @@
|
|
|
1
|
+
require "active_support/core_ext/object/blank"
|
|
1
2
|
require "loofah"
|
|
2
3
|
|
|
3
4
|
module UniversalRenderer
|
|
4
5
|
module SSR
|
|
6
|
+
# Removes executable content from HTML returned by the SSR service while
|
|
7
|
+
# preserving the elements and data attributes needed to hydrate an app.
|
|
8
|
+
#
|
|
9
|
+
# A blocklist over the whole HTML grammar cannot be a boundary against
|
|
10
|
+
# attacker-controlled markup, because the sanitizer and the browser have to
|
|
11
|
+
# agree on how the document parses. Treat it as defense in depth over HTML
|
|
12
|
+
# your own renderer produced.
|
|
5
13
|
class Scrubber < ::Loofah::Scrubber
|
|
14
|
+
# Elements that change how the *rest* of the markup is tokenized, so the
|
|
15
|
+
# tree this scrubber inspects is not the tree the browser builds.
|
|
16
|
+
#
|
|
17
|
+
# `<noscript>` is raw text with scripting enabled (the browser) and markup
|
|
18
|
+
# without it (the sanitizer), so `<noscript><p title="</noscript><img src=x
|
|
19
|
+
# onerror=...>">` reaches the browser as a live element. The MathML three
|
|
20
|
+
# flip the parser between foreign content and HTML integration points for
|
|
21
|
+
# the same effect. None belong in a server render.
|
|
22
|
+
PARSER_CONTEXT_ELEMENTS = %w[
|
|
23
|
+
noscript mglyph malignmark annotation-xml
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
BLOCKED_ELEMENTS = (
|
|
27
|
+
%w[
|
|
28
|
+
base embed frame frameset iframe object script
|
|
29
|
+
animate animatemotion animatetransform set
|
|
30
|
+
] + PARSER_CONTEXT_ELEMENTS
|
|
31
|
+
).freeze
|
|
32
|
+
# Non-executable data-script MIME types. HTML treats a script whose type is
|
|
33
|
+
# neither a JavaScript MIME type nor one of the special types (`module`,
|
|
34
|
+
# `importmap`, `speculationrules`) as an inert data block.
|
|
35
|
+
#
|
|
36
|
+
# The exception exists for JSON-LD, which the render emits and which would
|
|
37
|
+
# otherwise be sanitized away along with the SEO that motivates SSR.
|
|
38
|
+
ALLOWED_SCRIPT_TYPES = %w[application/json application/ld+json].freeze
|
|
39
|
+
URI_ATTRIBUTES = %w[
|
|
40
|
+
action background cite codebase data formaction href longdesc poster src
|
|
41
|
+
srcset xlink:href
|
|
42
|
+
].freeze
|
|
43
|
+
ALLOWED_PROTOCOLS = %w[http https mailto tel].freeze
|
|
44
|
+
DANGEROUS_PROTOCOL = /\A(?:javascript|vbscript):/i
|
|
45
|
+
DANGEROUS_DATA = %r{\Adata:(?:text/html|application/xhtml\+xml|image/svg\+xml)}i
|
|
46
|
+
SAFE_DATA_IMAGE = %r{\Adata:image/(?:avif|gif|jpeg|png|webp);base64,}i
|
|
47
|
+
# An SVG referenced by <img> renders in a restricted mode: no scripts, no
|
|
48
|
+
# external references. Bundlers routinely inline icons as data URIs, so the
|
|
49
|
+
# render would otherwise lose them. `<source>` inside `<picture>` feeds the
|
|
50
|
+
# same mode; inside `<video>`/`<audio>` it does not.
|
|
51
|
+
INLINE_SVG_DATA = %r{\Adata:image/svg\+xml[,;]}i
|
|
52
|
+
IMAGE_SOURCE_ATTRIBUTES = %w[src srcset].freeze
|
|
53
|
+
|
|
6
54
|
def initialize
|
|
7
55
|
super
|
|
8
56
|
@direction = :top_down
|
|
9
57
|
end
|
|
10
58
|
|
|
11
59
|
def scrub(node)
|
|
12
|
-
|
|
13
|
-
return Loofah::Scrubber::STOP if handle_script_node(node) # Checks for <script> and removes it
|
|
60
|
+
return Loofah::Scrubber::CONTINUE unless node.element?
|
|
14
61
|
|
|
15
|
-
|
|
16
|
-
|
|
62
|
+
if blocked_element?(node) || refreshing_meta?(node)
|
|
63
|
+
node.remove
|
|
64
|
+
return Loofah::Scrubber::STOP
|
|
65
|
+
end
|
|
17
66
|
|
|
18
|
-
# For all other nodes, clean potentially harmful attributes.
|
|
19
67
|
clean_attributes(node)
|
|
20
|
-
|
|
68
|
+
Loofah::Scrubber::CONTINUE
|
|
21
69
|
end
|
|
22
70
|
|
|
23
71
|
private
|
|
24
72
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return false unless node.name == "script"
|
|
73
|
+
def blocked_element?(node)
|
|
74
|
+
return false if node.name.downcase == "script" && data_script?(node)
|
|
28
75
|
|
|
29
|
-
node.
|
|
30
|
-
true # Indicates the node was a script and has been handled.
|
|
76
|
+
BLOCKED_ELEMENTS.include?(node.name.downcase)
|
|
31
77
|
end
|
|
32
78
|
|
|
33
|
-
#
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
79
|
+
# Scripts with no explicit type are JavaScript, so they stay blocked.
|
|
80
|
+
def data_script?(node)
|
|
81
|
+
type = node["type"].to_s.downcase.strip
|
|
82
|
+
return false if type.blank?
|
|
83
|
+
|
|
84
|
+
ALLOWED_SCRIPT_TYPES.include?(type.split(";", 2).first.strip)
|
|
37
85
|
end
|
|
38
86
|
|
|
39
|
-
|
|
87
|
+
def refreshing_meta?(node)
|
|
88
|
+
node.name.downcase == "meta" &&
|
|
89
|
+
node["http-equiv"]&.strip&.casecmp?("refresh")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Attribute *nodes*, not the `attributes` hash: that hash is keyed by local
|
|
93
|
+
# name, so under the HTML5 parser `xlink:href` arrives as `"href"` and
|
|
94
|
+
# `node["href"]` returns nil. An empty value read as safe, which left
|
|
95
|
+
# `<svg><a xlink:href="javascript:...">` intact.
|
|
40
96
|
def clean_attributes(node)
|
|
41
|
-
|
|
42
|
-
|
|
97
|
+
node.attribute_nodes.each do |attribute|
|
|
98
|
+
normalized_name = qualified_name(attribute)
|
|
99
|
+
remove_attribute =
|
|
100
|
+
normalized_name.start_with?("on") ||
|
|
101
|
+
normalized_name == "srcdoc" ||
|
|
102
|
+
(URI_ATTRIBUTES.include?(normalized_name) &&
|
|
103
|
+
unsafe_uri?(attribute.value.to_s, normalized_name, node))
|
|
104
|
+
|
|
105
|
+
attribute.remove if remove_attribute
|
|
106
|
+
end
|
|
43
107
|
end
|
|
44
108
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
109
|
+
# The name as written in the markup. HTML4 keeps the prefix in the name;
|
|
110
|
+
# HTML5 splits it into a namespace.
|
|
111
|
+
def qualified_name(attribute)
|
|
112
|
+
prefix = attribute.namespace&.prefix
|
|
113
|
+
name = attribute.name.to_s
|
|
114
|
+
(prefix ? "#{prefix}:#{name}" : name).downcase
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The parser has already resolved character references, so `value` is literal
|
|
118
|
+
# text. Decoding it again would only corrupt URLs that legitimately contain
|
|
119
|
+
# an escaped entity.
|
|
120
|
+
def unsafe_uri?(value, attribute_name, node)
|
|
121
|
+
normalized = value.gsub(/[\u0000-\u0020]/, "")
|
|
122
|
+
return true if normalized.match?(DANGEROUS_PROTOCOL)
|
|
123
|
+
return false if inline_svg_image?(normalized, attribute_name, node)
|
|
124
|
+
return true if normalized.match?(DANGEROUS_DATA)
|
|
125
|
+
|
|
126
|
+
scheme = normalized[/\A([a-z][a-z0-9+.-]*):/i, 1]
|
|
127
|
+
return false unless scheme
|
|
128
|
+
return false if ALLOWED_PROTOCOLS.include?(scheme.downcase)
|
|
129
|
+
return false if %w[src srcset].include?(attribute_name) &&
|
|
130
|
+
normalized.match?(SAFE_DATA_IMAGE)
|
|
131
|
+
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def inline_svg_image?(uri, attribute_name, node)
|
|
136
|
+
return false unless IMAGE_SOURCE_ATTRIBUTES.include?(attribute_name)
|
|
137
|
+
return false unless restricted_image_context?(node)
|
|
138
|
+
|
|
139
|
+
uri.match?(INLINE_SVG_DATA)
|
|
50
140
|
end
|
|
51
141
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
attrs_to_remove.each { |attr_name| node.remove_attribute(attr_name) }
|
|
142
|
+
def restricted_image_context?(node)
|
|
143
|
+
case node.name.downcase
|
|
144
|
+
when "img" then true
|
|
145
|
+
when "source" then node.parent&.name&.downcase == "picture"
|
|
146
|
+
else false
|
|
147
|
+
end
|
|
59
148
|
end
|
|
60
149
|
end
|
|
61
150
|
end
|
data/lib/universal_renderer.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: universal_renderer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thaske
|
|
@@ -70,6 +70,13 @@ files:
|
|
|
70
70
|
- Rakefile
|
|
71
71
|
- lib/generators/universal_renderer/install_generator.rb
|
|
72
72
|
- lib/generators/universal_renderer/templates/initializer.rb
|
|
73
|
+
- lib/generators/universal_renderer/templates/ssr.rake
|
|
74
|
+
- lib/generators/universal_renderer/templates/ssr/config.ts
|
|
75
|
+
- lib/generators/universal_renderer/templates/ssr/dev.ts
|
|
76
|
+
- lib/generators/universal_renderer/templates/ssr/globals.ts
|
|
77
|
+
- lib/generators/universal_renderer/templates/ssr/server.ts
|
|
78
|
+
- lib/generators/universal_renderer/templates/vite.config.ssr.mts
|
|
79
|
+
- lib/generators/universal_renderer/templates/web
|
|
73
80
|
- lib/tasks/universal_renderer_tasks.rake
|
|
74
81
|
- lib/universal_renderer.rb
|
|
75
82
|
- lib/universal_renderer/client/base.rb
|
|
@@ -80,6 +87,7 @@ files:
|
|
|
80
87
|
- lib/universal_renderer/client/stream/setup.rb
|
|
81
88
|
- lib/universal_renderer/configuration.rb
|
|
82
89
|
- lib/universal_renderer/engine.rb
|
|
90
|
+
- lib/universal_renderer/instrumentation.rb
|
|
83
91
|
- lib/universal_renderer/renderable.rb
|
|
84
92
|
- lib/universal_renderer/ssr/helpers.rb
|
|
85
93
|
- lib/universal_renderer/ssr/placeholders.rb
|