@@ -41,9 +41,10 @@ describe "Document Elements" do
4141 body.render.should eq(" <body class=\" main\" ></body>" )
4242 end
4343
44- it " accepts event handlers" do
45- body = Components ::Elements ::Body .new(onload: " init()" )
46- body[" onload" ].should eq(" init()" )
44+ it " rejects inline event-handler attributes (SafeHTML v1 on* ban)" do
45+ expect_raises(ArgumentError , " inline event-handler attribute" ) do
46+ Components ::Elements ::Body .new(onload: " init()" )
47+ end
4748 end
4849 end
4950
@@ -130,76 +131,103 @@ describe "Document Elements" do
130131 end
131132
132133 describe Components ::Elements ::Style do
133- it " renders style element with CSS" do
134- style = Components ::Elements ::Style .new
135- style << " body { margin: 0; }"
134+ # SafeHTML v1 (docs/SAFE_HTML_V1.md §3.8): a plain `String` child is
135+ # banned — `</style>` breaks out of the element and lets a following
136+ # `<script>` execute, exactly like the `<script>`-body ban. Use
137+ # `Style.css(css, reason:)`. See
138+ # `spec/web/components/safe/style_element_safety_spec.cr` for the full
139+ # adversarial suite.
140+ it " renders style element with CSS via the static-CSS door" do
141+ style = Components ::Elements ::Style .css(" body { margin: 0; }" , reason: " spec: static CSS literal" )
136142 style.render.should eq(" <style>body { margin: 0; }</style>" )
137143 end
138-
139- it " can be initialized with CSS content" do
140- style = Components ::Elements ::Style .new(" h1 { color: blue; }" )
141- style.render.should eq(" <style>h1 { color: blue; }</style>" )
144+
145+ it " Style.css requires a non-empty reason" do
146+ expect_raises(ArgumentError , " requires a non-empty" ) do
147+ Components ::Elements ::Style .css(" h1 { color: blue; }" , reason: " " )
148+ end
142149 end
143-
144- it " only accepts text content " do
150+
151+ it " rejects a plain String child " do
145152 style = Components ::Elements ::Style .new
146-
153+
154+ expect_raises(ArgumentError , " does not accept a plain String child" ) do
155+ style << " .class > div { color: red; }"
156+ end
157+ end
158+
159+ it " only accepts CSS text (via Style.css), not other HTML elements" do
160+ style = Components ::Elements ::Style .new
161+
147162 expect_raises(ArgumentError , " Style element should only contain CSS text" ) do
148163 style << Components ::Elements ::Html .new
149164 end
150165 end
151-
152- it " does not escape CSS content" do
153- style = Components ::Elements ::Style .new
154- style << " .class > div { color: red; }"
166+
167+ it " does not escape CSS content vouched through Style.css" do
168+ style = Components ::Elements ::Style .css(" .class > div { color: red; }" , reason: " spec: static CSS literal" )
155169 style.render.should contain(" .class > div { color: red; }" )
156170 end
157171 end
158172
159173 describe Components ::Elements ::Script do
160- it " renders script element with JavaScript" do
161- script = Components ::Elements ::Script .new
162- script << " console.log('Hello');"
174+ it " renders script element with JavaScript via the static-JS door" do
175+ script = Components ::Elements ::Script .static(" console.log('Hello');" , reason: " spec: static JS literal" )
163176 script.render.should eq(" <script>console.log('Hello');</script>" )
164177 end
165-
166- it " can be initialized with JavaScript content" do
167- script = Components ::Elements ::Script .new (" alert('Hi');" )
178+
179+ it " can be initialized with JavaScript content via the static-JS door " do
180+ script = Components ::Elements ::Script .static (" alert('Hi');" , reason: " spec: static JS literal " )
168181 script.render.should eq(" <script>alert('Hi');</script>" )
169182 end
170-
183+
184+ it " rejects a plain String child (SafeHTML v1 script-interpolation ban)" do
185+ script = Components ::Elements ::Script .new
186+
187+ expect_raises(ArgumentError , " does not accept a plain String child" ) do
188+ script << " console.log('should be banned');"
189+ end
190+ end
191+
171192 it " only accepts text content" do
172193 script = Components ::Elements ::Script .new
173-
194+
174195 expect_raises(ArgumentError , " Script element should only contain JavaScript text" ) do
175196 script << Components ::Elements ::Html .new
176197 end
177198 end
178-
199+
179200 it " validates boolean attributes" do
180201 script = Components ::Elements ::Script .new(async: " true" , defer: " " )
181202 script[" async" ].should eq(" true" )
182203 script[" defer" ].should eq(" " )
183-
204+
184205 expect_raises(ArgumentError , " async is a boolean attribute" ) do
185206 Components ::Elements ::Script .new(async: " yes" )
186207 end
187208 end
188-
209+
189210 it " validates crossorigin attribute" do
190211 Components ::Elements ::Script .new(crossorigin: " anonymous" )
191212 Components ::Elements ::Script .new(crossorigin: " use-credentials" )
192-
213+
193214 expect_raises(ArgumentError , " Invalid crossorigin value: invalid" ) do
194215 Components ::Elements ::Script .new(crossorigin: " invalid" )
195216 end
196217 end
197-
198- it " does not escape JavaScript content" do
199- script = Components ::Elements ::Script .new
200- script << " if (x < 10 && y > 5) { alert('test'); }"
218+
219+ it " does not escape JavaScript content on the static-JS door" do
220+ script = Components ::Elements ::Script .static(" if (x < 10 && y > 5) { alert('test'); }" , reason: " spec: static JS literal" )
201221 script.render.should contain(" if (x < 10 && y > 5) { alert('test'); }" )
202222 end
223+
224+ it " serializes data via the typed json_data helper and neutralizes </script> breakout" do
225+ script = Components ::Elements ::Script .json_data(" page-data" , {name: " </script><script>alert(1)</script>" })
226+ rendered = script.render
227+ rendered.should contain(%( <script id="page-data" type="application/json">) )
228+ rendered.should_not contain(" </script><script>alert(1)" )
229+ rendered.should contain(" <\\ /script" )
230+ end
203231 end
204232
205233 describe " Building a complete HTML document" do
@@ -215,10 +243,10 @@ describe "Document Elements" do
215243 end
216244
217245 doc << Components ::Elements ::Body .new.build do |body |
218- body << Components ::Elements ::Script .new (" console.log('Loaded');" )
246+ body << Components ::Elements ::Script .static (" console.log('Loaded');" , reason: " spec: static JS literal " )
219247 end
220248 end
221-
249+
222250 rendered = html.render
223251 rendered.should contain(" <html lang=\" en\" >" )
224252 rendered.should contain(" <meta charset=\" UTF-8\" >" )
0 commit comments