@@ -69,6 +69,22 @@ def testCreate(self):
6969 self .assertEqual (rv ["mode" ], 1 )
7070 self .assertEqual (rv ["text" ], '<p>Lorem ipsum ...</p>' )
7171
72+ def testWebsiteXSSPayloadIsEscaped (self ):
73+ """Website field with XSS payload must have quotes HTML-escaped."""
74+ payload = "http://x.com/?'onmouseover='alert(document.domain)'x='"
75+ rv = self .post ('/new?uri=%2Fpath%2F' ,
76+ data = json .dumps ({'text' : 'Hello' , 'website' : payload }))
77+ self .assertEqual (rv .status_code , 201 )
78+ rv = loads (rv .data )
79+ # Single quotes must be HTML-escaped so they cannot break out of an
80+ # HTML attribute context (e.g. href='...')
81+ self .assertNotIn ("'" , rv ["website" ])
82+ self .assertNotIn ('"' , rv ["website" ])
83+ self .assertEqual (
84+ rv ["website" ],
85+ "http://x.com/?'onmouseover='alert(document.domain)'x='" ,
86+ )
87+
7288 def textCreateWithNonAsciiText (self ):
7389
7490 rv = self .post ('/new?uri=%2Fpath%2F' ,
@@ -391,6 +407,27 @@ def testUpdate(self):
391407 self .assertEqual (rv ['website' ], 'http://example.com/' )
392408 self .assertIn ('modified' , rv )
393409
410+ def testUpdateWebsiteXSSPayloadIsEscaped (self ):
411+ """Website and author XSS payloads via edit endpoint must be HTML-escaped."""
412+ self .post ('/new?uri=%2Fpath%2F' , data = json .dumps ({'text' : 'Lorem ipsum ...' }))
413+
414+ website_payload = "http://x.com/?'onmouseover='alert(document.domain)'x='"
415+ author_payload = "<script>alert(1)</script>"
416+ self .put ('/id/1' , data = json .dumps ({
417+ 'text' : 'Hello World' ,
418+ 'author' : author_payload ,
419+ 'website' : website_payload ,
420+ }))
421+
422+ rv = loads (self .get ('/id/1?plain=1' ).data )
423+ self .assertNotIn ("'" , rv ["website" ])
424+ self .assertEqual (
425+ rv ["website" ],
426+ "http://x.com/?'onmouseover='alert(document.domain)'x='" ,
427+ )
428+ self .assertNotIn ("<script>" , rv ["author" ])
429+ self .assertEqual (rv ["author" ], "<script>alert(1)</script>" )
430+
394431 def testUpdateForbidden (self ):
395432
396433 self .post ('/new?uri=test' , data = json .dumps ({'text' : 'Hello world!' }))
@@ -860,6 +897,34 @@ def testModerateComment(self):
860897 # Comment should no longer exist
861898 self .assertEqual (self .app .db .comments .get (id_ ), None )
862899
900+ def testModerateEditXSSPayloadIsEscaped (self ):
901+ """XSS payloads in author/website via moderate edit endpoint must be HTML-escaped."""
902+ id_ = 1
903+ signed = self .app .sign (id_ )
904+
905+ self .client .post ('/new?uri=/moderated' , data = json .dumps ({"text" : "..." }))
906+
907+ website_payload = "http://x.com/?'onmouseover='alert(document.domain)'x='"
908+ author_payload = "<script>alert(1)</script>"
909+ rv = self .client .post (
910+ '/id/%d/edit/%s' % (id_ , signed ),
911+ data = json .dumps ({
912+ "text" : "new text" ,
913+ "author" : author_payload ,
914+ "website" : website_payload ,
915+ }),
916+ )
917+ self .assertEqual (rv .status_code , 200 )
918+
919+ stored = self .app .db .comments .get (id_ )
920+ self .assertNotIn ("'" , stored ["website" ])
921+ self .assertEqual (
922+ stored ["website" ],
923+ "http://x.com/?'onmouseover='alert(document.domain)'x='" ,
924+ )
925+ self .assertNotIn ("<script>" , stored ["author" ])
926+ self .assertEqual (stored ["author" ], "<script>alert(1)</script>" )
927+
863928
864929class TestUnsubscribe (unittest .TestCase ):
865930
0 commit comments