Modify

Opened 2 years ago

Closed 21 months ago

#14174 closed defect (fixed)

Possibilities of SQL injection and cross-site scripting

Reported by: Jun Omae Owned by: Ralph Ewig
Priority: high Component: TracChecklistMacro
Severity: critical Keywords:
Cc: Trac Release: 1.4

Attachments (1)

checklistmacro-xss.png (41.1 KB) - added by Jun Omae 22 months ago.

Download all attachments as: .zip

Change History (16)

comment:1 Changed 2 years ago by Ralph Ewig

Status: newaccepted

Good catch, thank you for reporting this (and the documentation link).

comment:2 Changed 22 months ago by Ralph Ewig

Priority: normalhigh
Resolution: fixed
Status: acceptedclosed
Trac Release: 1.4

Fixed in version 0.1.6: tracchecklistmacro?rev=18503

FIXED updated all sql to follow TracDev Guidelines
DONE used tag constructors (in some places)

The query changes might help with cross-db compatibility, but I've only tested using MariaDB/MySQL as the backend.

The tag constructors require generating html from the "inside-out", rather than "top-down" which was awkward in places. Since their use is not security related (as I understand it), I only used them were they made the code more convenient.

comment:3 Changed 22 months ago by Jun Omae

It seem that XSS attack is able via name parameter at source:/tracchecklistmacro/0.1/checklist/macros.py@18503#L131

-        head  = "<form class='checklist' method='post' action='"+dest+"'> \n"
-        head += "   <div class='source'>                                  \n"
-        head += ("       TracChecklist: <a href='" + formatter.req.href("/wiki/" + name) +
-                 "'>" + name + "</a>\n")
-        head += "   </div>"
+        head = tag.form(class_='checklist', method='post', action=dest)
+        head.append(tag.div('TracChecklist: ',
+                            tag.a(name, href=formatter.req.href('wiki', name)),
+                            class_='source'))
+        head = unicode(head)
         # template footer / form
         backpath   = formatter.req.href(formatter.req.path_info)
         form_token = formatter.req.form_token
         realm      = str(formatter.resource.realm)
         resource   = str(formatter.resource.id   )
         checklist  = str(content                 )
-        foot  = Markup(Element('input', type='hidden', name='__backpath__', value=backpath         ))
-        foot += Markup(Element('input', type='hidden', name='__FORM_TOKEN', value=form_token       ))
-        foot += Markup(Element('input', type='hidden', name='realm'       , value=realm            ))
-        foot += Markup(Element('input', type='hidden', name='resource'    , value=resource         ))
-        foot += Markup(Element('input', type='hidden', name='checklist'   , value=checklist        ))
-        foot += Markup(Element('input', type='submit', class_='button'    , value='Save Checklist' ))
-        foot += "</form>"
+        foot = unicode(tag(
+            tag.input(type='hidden', name='__backpath__', value=backpath         ),
+            tag.input(type='hidden', name='__FORM_TOKEN', value=form_token       ),
+            tag.input(type='hidden', name='realm'       , value=realm            ),
+            tag.input(type='hidden', name='resource'    , value=resource         ),
+            tag.input(type='hidden', name='checklist'   , value=checklist        ),
+            tag.input(type='submit', class_='button'    , value='Save Checklist' ),
+        ))

comment:4 Changed 22 months ago by Ralph Ewig

How would you exploit that though? Not that I don't believe you, just trying to understand. From what I can tell, both methods (using the tag constructor vs. string coding) result in the same source for the generated page, so I'm not sure how one is vulnerable but not the other?

comment:5 Changed 22 months ago by Jun Omae

  1. Create wiki page TracChecklist/<a href="javascript:alert(42)">XSS</a> with [[Checklist(<a href="javascript:alert(42)">XSS</a>)]].
    1. Visit SandBox page, add [wiki:'TracChecklist/<a href="javascript:alert(42)">XSS</a>'] to the page, and save it.
    2. Click the added link to TracChecklist/<a href="javascript:alert(42)">XSS</a> page.
    3. Click "Create this page" button
    4. Store [[Checklist(<a href="javascript:alert(42)">XSS</a>)]] in the page.
  2. Click XSS

(Edit: The page content is revised)

Last edited 22 months ago by Jun Omae (previous) (diff)

Changed 22 months ago by Jun Omae

Attachment: checklistmacro-xss.png added

comment:6 Changed 22 months ago by Jun Omae

Another thing, Option usage in the plugin is wrong.

  • checklist/macros.py

     
    3838    _description = "Inserts a checklist sourced from a wiki page template."
    3939    implements(ITemplateProvider)
    4040
     41    template_root = Option(
     42        'checklist', 'template_root', default='TracChecklist',
     43        doc="wiki page root for checklist templates")
     44
    4145    def get_htdocs_dirs(self):
    4246        return [('checklist', resource_filename(__name__, 'htdocs'))]
    4347
     
    8488                check_dict = json.loads(row[0])
    8589
    8690        # get the template wiki page (create if needed)
    87         root = Option('checklist', 'template_root', default='TracChecklist',
    88                     doc="wiki page root for checklist templates")
    89 
    90         root = self.config.get('checklist', 'template_root')
     91        root = self.template_root
    9192        name = str(root) + '/' + content
    9293        name = name.replace('//','/')
    9394        page = WikiPage(self.env, name)
     
    262263
    263264            # list of available checklist templates (pages) under root wiki page
    264265            templates = []
    265             root  = Option('checklist', 'template_root', default='TracChecklist',
    266                             doc="wiki page root for checklist templates")
    267             root  = self.config.get('checklist', 'template_root')
     266            root  = self.template_root
    268267            query = "SELECT name FROM wiki WHERE version = 1 AND name LIKE %s"
    269268            param = ['%'+root+'%']
    270269

comment:7 in reply to:  3 ; Changed 22 months ago by Ralph Ewig

Super helpful - thank you. The tag.append() method is great, but I'm unsure how to append the raw html from the converted wiki page? Right now I have:

html = format_to_html(self.env, formatter.context, wiki)
html = ... (replace '[x]' with '<input type=checkbox> ...'

head = ... (some html)
foot = ... (some html)

out  = unescape(head + html + foot)

So, I tried to construct the form instead using:

html = format_to_html(self.env, formatter.context, wiki)
html = ... (replace '[x]' with '<input type=checkbox> ...'

form = tag.form (...
head = tag.div(...
foot = tag(...

form.append(head)
form.append(html)
form.append(foot)
out = unicode(form)

But this strips the checkbox functionality from the converted wiki page?

Last edited 22 months ago by Ralph Ewig (previous) (diff)

comment:8 Changed 22 months ago by Ralph Ewig

Resolution: fixed
Status: closedreopened

comment:9 Changed 22 months ago by Ralph Ewig

Status: reopenedaccepted

comment:10 in reply to:  7 Changed 22 months ago by Jun Omae

But this strips the checkbox functionality from the converted wiki page?

I'm afraid what do "this strips the checkbox functionality" mean? It seems that work like the following. Please explain it.

>>> from trac.test import EnvironmentStub
>>> from trac.wiki.formatter import format_to_html
>>> from trac.web.chrome import web_context
>>> from trac.test import MockRequest
>>> from trac.util.html import tag
>>> env = EnvironmentStub()
>>> req = MockRequest(env)
>>> context = web_context(req)
>>> rv = format_to_html(env, context, '[x] text')
>>> rv
Markup(u'<p>\n[x] text\n</p>\n')
>>> rv = rv.replace('[x]', tag.input(type='checkbox'))
>>> rv
Markup(u'<p>\n<input type="checkbox" /> text\n</p>\n')
>>> form = tag.form()
>>> form.append(rv)
>>> unicode(form)
u'<form><p>\n<input type="checkbox" /> text\n</p>\n</form>'

comment:11 Changed 22 months ago by Ralph Ewig

The way the plugin works, a user first creates a checklist template as a wiki page ("edit mode") and that template is then reused to create the clickable checklist in many tickets ("live mode"). In the wiki page (template), a "step" to be checked off later is identified with a pattern of [x] some description \\.

To generate the page for "live mode", I do the following steps:

  1. load the wiki page (template) from the database
  2. convert page to html using format_to_html(self.env, formatter.context, wiki)
  3. replace string pattern [x] some description \\ with checkbox code <input ...>
  4. wrap the result in a form, and insert it in the ticket display

When I use tag constructors for step 4, the part I'm struggling with is how to keep the html formatting (step 2) and check boxes (step 3), but I think your example above can do that (I'll try it).

Last edited 22 months ago by Ralph Ewig (previous) (diff)

comment:12 Changed 22 months ago by Jun Omae

The procedure which replaces [x] markers in html with checkboxes is buggy. If forgetting \\ at end of line, it is busy loop and eats huge memory (as the results, oom-killer is called).

It could be simplified using re.sub().

  • checklist/macros.py

     
    33# Macro that pulls a checklist template from a wiki page, and renders it as
    44# a checklist in a ticket description.
    55#
    6 import json, time
     6import json, re, time
    77import pkg_resources
    88from pkg_resources import resource_filename
    99
     
    9696            return("CHECKLIST ERROR: \n template '" + name + "' does not exist.")
    9797
    9898        # strip wiki page header & convert to html
    99         wiki = page.text[page.text.find('----')+4:]
     99        wiki = page.text
     100        sep = wiki.find('----')
     101        if sep != -1:
     102            wiki = wiki[sep + 4:]
    100103        html = format_to_html(self.env, formatter.context, wiki)
    101104
    102105        # parse template for steps
    103         # steps start with [x] and end with \\, which converts to <br />
    104         check_index = 0
    105         check_start = html.find('[x]')
     106        cbox_re = re.compile(r'\s*\[x\]([^<]+)(<br\s*/?>)?', re.DOTALL)
     107        cbox_idx = [0]
    106108
    107         while check_start > -1:
    108             check_end   = html.find('<br />',check_start)+6
    109             check_step  = 'step_' + str(check_index)
    110             check_label = html[check_start+3:check_end]
     109        def cbox_repl(match):
     110            label = match.group(1)
     111            cbox_name = 'step_%d' % cbox_idx[0]
     112            cbox_idx[0] += 1
     113            br = match.group(2)
     114            return (u'<label><input type="checkbox" name="{}" value="done" />'
     115                    u'{}</label>{}').format(cbox_name, label, br)
    111116
    112             check_box   = ("<label class='container'>" + check_label +
    113                            "<input type='checkbox' " +
    114                            "name='step_" + str(check_index) + "' " +
    115                            "value='done' ")
     117        html = cbox_re.sub(cbox_repl, html)
    116118
    117             if check_step in check_dict:
    118                 check_box += "checked"
    119 
    120             check_box += "><span class='checkmark'></span></label>"
    121             html = html[0:check_start] + check_box + html[check_end:]
    122 
    123             check_index +=1
    124             check_start = html.find('[x]')
    125 
    126119        # template header info + form
    127120        dest  = formatter.req.href('/checklist/update')
    128121        head  = "<form class='checklist' method='post' action='"+dest+"'> \n"

comment:13 Changed 21 months ago by Ralph Ewig

I think I fixed it in v0.1.7. Also improved the conversion of the wiki checklist template into the html checklist instance to make it more robust. It uploaded without error to PyPi and svn, but doesn't show yet in trac-hacks for some reason.

comment:14 in reply to:  13 Changed 21 months ago by Jun Omae

Replying to Ralph Ewig:

It uploaded without error to PyPi and svn, but doesn't show yet in trac-hacks for some reason.

The issue has been fixed by Ryan.

comment:15 Changed 21 months ago by Ralph Ewig

Resolution: fixed
Status: acceptedclosed

Great! I just uploaded v0.1.8 which should have everything linted/formatted/spellchecked and a couple more small tweaks to make the code more robust. I'll close this as fixed, but please let me know if anything else needs attention.

Modify Ticket

Change Properties
Set your email in Preferences
Action
as closed The owner will remain Ralph Ewig.
The resolution will be deleted. Next status will be 'reopened'.

Add Comment


E-mail address and name can be saved in the Preferences.

 
Note: See TracTickets for help on using tickets.