@@ -214,4 +214,150 @@ describe('styleObjectForEach', () => {
214214 )
215215 } )
216216 } )
217+
218+ describe ( 'Should skip values containing ";" to prevent CSS injection' , ( ) => {
219+ it ( 'skips a value with ";"' , ( ) => {
220+ const fn = vi . fn ( )
221+ styleObjectForEach ( { color : 'red;background:blue' } , fn )
222+ expect ( fn ) . not . toBeCalled ( )
223+ } )
224+
225+ it ( 'keeps safe siblings while skipping unsafe ones' , ( ) => {
226+ const fn = vi . fn ( )
227+ styleObjectForEach ( { color : 'red;background:blue' , backgroundColor : 'white' } , fn )
228+ expect ( fn ) . toBeCalledTimes ( 1 )
229+ expect ( fn ) . toBeCalledWith ( 'background-color' , 'white' )
230+ } )
231+
232+ it ( 'does not throw for unsafe values' , ( ) => {
233+ expect ( ( ) => styleObjectForEach ( { color : 'a;b' } , ( ) => { } ) ) . not . toThrow ( )
234+ } )
235+
236+ it ( 'keeps semicolons inside quoted strings' , ( ) => {
237+ const fn = vi . fn ( )
238+ styleObjectForEach ( { fontFamily : '"a;b", sans-serif' } , fn )
239+ expect ( fn ) . toBeCalledWith ( 'font-family' , '"a;b", sans-serif' )
240+ } )
241+
242+ test . each ( [
243+ [ 'LF' , '\n' ] ,
244+ [ 'CR' , '\r' ] ,
245+ [ 'FF' , '\f' ] ,
246+ ] ) ( 'skips quoted strings that contain %s before a declaration separator' , ( _ , newline ) => {
247+ const fn = vi . fn ( )
248+ styleObjectForEach (
249+ { fontFamily : `"${ newline } ;background:url(https://example.com/a.png)` } ,
250+ fn
251+ )
252+ expect ( fn ) . not . toBeCalled ( )
253+ } )
254+
255+ it ( 'keeps semicolons inside CSS functions' , ( ) => {
256+ const fn = vi . fn ( )
257+ styleObjectForEach ( { backgroundImage : 'url("data:image/svg+xml;utf8,<svg></svg>")' } , fn )
258+ expect ( fn ) . toBeCalledWith ( 'background-image' , 'url("data:image/svg+xml;utf8,<svg></svg>")' )
259+ } )
260+
261+ test . each ( [ [ 'square brackets' , 'red[;background:blue]' ] ] ) (
262+ 'keeps semicolons inside CSS simple blocks with %s' ,
263+ ( _ , value ) => {
264+ const fn = vi . fn ( )
265+ styleObjectForEach ( { color : value } , fn )
266+ expect ( fn ) . toBeCalledWith ( 'color' , value )
267+ }
268+ )
269+
270+ test . each ( [
271+ [ 'opening curly block' , 'red{;background:blue}' ] ,
272+ [ 'closing curly block' , 'red};background:blue' ] ,
273+ ] ) ( 'skips CSS values containing %s' , ( _ , value ) => {
274+ const fn = vi . fn ( )
275+ styleObjectForEach ( { color : value , backgroundColor : 'white' } , fn )
276+ expect ( fn ) . toBeCalledTimes ( 1 )
277+ expect ( fn ) . toBeCalledWith ( 'background-color' , 'white' )
278+ } )
279+
280+ test . each ( [
281+ [ 'square brackets' , 'red[;background:blue];position:fixed' ] ,
282+ [ 'curly braces' , 'red{;background:blue};position:fixed' ] ,
283+ ] ) ( 'skips top-level semicolons after CSS simple blocks with %s' , ( _ , value ) => {
284+ const fn = vi . fn ( )
285+ styleObjectForEach ( { color : value } , fn )
286+ expect ( fn ) . not . toBeCalled ( )
287+ } )
288+
289+ it ( 'skips top-level semicolons after CSS comments' , ( ) => {
290+ const fn = vi . fn ( )
291+ styleObjectForEach ( { color : 'red/*(*/;background:blue;position:fixed;top:0' } , fn )
292+ expect ( fn ) . not . toBeCalled ( )
293+ } )
294+
295+ test . each ( [
296+ [ 'comment' , 'red/*' ] ,
297+ [ 'double-quoted string' , '"abc' ] ,
298+ [ 'single-quoted string' , "'abc" ] ,
299+ [ 'function block' , 'red(' ] ,
300+ [ 'square block' , 'red[' ] ,
301+ [ 'curly block' , 'red{' ] ,
302+ [ 'unmatched function closer' , 'red)' ] ,
303+ [ 'unmatched square closer' , 'red]' ] ,
304+ [ 'unmatched curly closer' , 'red}' ] ,
305+ [ 'mismatched simple block closer' , 'red[)' ] ,
306+ [ 'escape' , 'red\\' ] ,
307+ ] ) ( 'skips unterminated CSS %s that can swallow following declarations' , ( _ , value ) => {
308+ const fn = vi . fn ( )
309+ styleObjectForEach ( { color : value , display : 'none' } , fn )
310+ expect ( fn ) . toBeCalledTimes ( 1 )
311+ expect ( fn ) . toBeCalledWith ( 'display' , 'none' )
312+ } )
313+
314+ test . each ( [
315+ [ 'comment that closes exactly at end of value' , 'red/* hi */' ] ,
316+ [ 'CSS hex escape that decodes to a delimiter byte' , 'red\\3b ' ] ,
317+ [ 'vertical tab inside a quoted string' , '"\vfoo"' ] ,
318+ [ 'trailing solidus that is not a comment start' , 'a/' ] ,
319+ ] ) ( 'keeps CSS-safe edge case — %s' , ( _ , value ) => {
320+ const fn = vi . fn ( )
321+ styleObjectForEach ( { color : value } , fn )
322+ expect ( fn ) . toBeCalledWith ( 'color' , value )
323+ } )
324+
325+ it ( 'skips style property names that can break declaration boundaries' , ( ) => {
326+ const fn = vi . fn ( )
327+ styleObjectForEach ( { 'color;background-image' : 'url(https://example.com/a.png)' } , fn )
328+ styleObjectForEach ( { 'color:background' : 'red' } , fn )
329+ expect ( fn ) . not . toBeCalled ( )
330+ } )
331+
332+ it ( 'keeps safe property names that use digits' , ( ) => {
333+ const fn = vi . fn ( )
334+ styleObjectForEach ( { '--my-var-1' : '15px' , '--myVar-2' : '20px' } , fn )
335+ expect ( fn ) . toHaveBeenNthCalledWith ( 1 , '--my-var-1' , '15px' )
336+ expect ( fn ) . toHaveBeenNthCalledWith ( 2 , '--myVar-2' , '20px' )
337+ } )
338+
339+ it ( 'keeps vendor-prefixed property names' , ( ) => {
340+ const fn = vi . fn ( )
341+ styleObjectForEach ( { WebkitLineClamp : '2' } , fn )
342+ expect ( fn ) . toBeCalledWith ( '-webkit-line-clamp' , '2' )
343+ } )
344+
345+ it ( 'keeps validating style property names after the valid style property name cache is reset' , ( ) => {
346+ for ( let i = 0 ; i < 1025 ; i ++ ) {
347+ styleObjectForEach ( { [ `--cache-${ i } ` ] : '1px' } , ( ) => { } )
348+ }
349+
350+ const fn = vi . fn ( )
351+ styleObjectForEach ( { '--after-reset-1' : '2px' , 'color:background' : 'red' } , fn )
352+ expect ( fn ) . toBeCalledTimes ( 1 )
353+ expect ( fn ) . toBeCalledWith ( '--after-reset-1' , '2px' )
354+ } )
355+
356+ it ( 'skips unsupported runtime values without throwing' , ( ) => {
357+ const fn = vi . fn ( )
358+ expect ( ( ) => styleObjectForEach ( { color : false , backgroundColor : 'white' } , fn ) ) . not . toThrow ( )
359+ expect ( fn ) . toBeCalledTimes ( 1 )
360+ expect ( fn ) . toBeCalledWith ( 'background-color' , 'white' )
361+ } )
362+ } )
217363} )
0 commit comments