Changeset - ddad3be4dc44
[Not reviewed]
stable
0 1 0
Thomas De Schampheleire - 7 years ago 2019-04-19 20:54:46
thomas.de_schampheleire@nokia.com
changeset: fix XSS vulnerability in parent-child navigation

The 'Parent Rev.' - 'Child Rev.' links on changesets and in the file browser
normally immediately jump to the correct revision upon click. But, if there
are multiple candidates, e.g. two children of a commit, then a list of
revisions is shown as hyperlinks instead.

These hyperlinks have a 'title' attribute containing the full commit message
of the corresponding commit. When this commit message contains characters
special to HTML, like ", >, etc. they were added literally to the HTML code.

This can lead to a cross-site scripting (XSS) vulnerability when an attacker
has write access to a repository. They could craft a special commit message
that would introduce HTML and/or JavaScript code when the commit is listed
in such 'parent-child' navigation links.

Escape the commit message before using it further.
1 file changed with 1 insertions and 1 deletions:
0 comments (0 inline, 0 general)
kallithea/public/js/base.js
Show inline comments
 
@@ -1112,398 +1112,398 @@ var autocompleteFormatter = function (oR
 
        return autocompleteGravatar(displayname, oResultData.gravatar_lnk, oResultData.gravatar_size);
 
    }
 

	
 
    return '';
 
};
 

	
 
var SimpleUserAutoComplete = function ($inputElement) {
 
    $inputElement.select2({
 
        formatInputTooShort: $inputElement.attr('placeholder'),
 
        initSelection : function (element, callback) {
 
            $.ajax({
 
                url: pyroutes.url('users_and_groups_data'),
 
                dataType: 'json',
 
                data: {
 
                    key: element.val()
 
                },
 
                success: function(data){
 
                  callback(data.results[0]);
 
                }
 
            });
 
        },
 
        minimumInputLength: 1,
 
        ajax: {
 
            url: pyroutes.url('users_and_groups_data'),
 
            dataType: 'json',
 
            data: function(term, page){
 
              return {
 
                query: term
 
              };
 
            },
 
            results: function (data, page){
 
              return data;
 
            },
 
            cache: true
 
        },
 
        formatSelection: autocompleteFormatter,
 
        formatResult: autocompleteFormatter,
 
        id: function(item) { return item.nname; },
 
    });
 
}
 

	
 
var MembersAutoComplete = function ($inputElement, $typeElement) {
 

	
 
    $inputElement.select2({
 
        placeholder: $inputElement.attr('placeholder'),
 
        minimumInputLength: 1,
 
        ajax: {
 
            url: pyroutes.url('users_and_groups_data'),
 
            dataType: 'json',
 
            data: function(term, page){
 
              return {
 
                query: term,
 
                types: 'users,groups'
 
              };
 
            },
 
            results: function (data, page){
 
              return data;
 
            },
 
            cache: true
 
        },
 
        formatSelection: autocompleteFormatter,
 
        formatResult: autocompleteFormatter,
 
        id: function(item) { return item.type == 'user' ? item.nname : item.grname },
 
    }).on("select2-selecting", function(e) {
 
        // e.choice.id is automatically used as selection value - just set the type of the selection
 
        $typeElement.val(e.choice.type);
 
    });
 
}
 

	
 
var MentionsAutoComplete = function ($inputElement) {
 
  $inputElement.atwho({
 
    at: "@",
 
    callbacks: {
 
      remoteFilter: function(query, callback) {
 
        $.getJSON(
 
          pyroutes.url('users_and_groups_data'),
 
          {
 
            query: query,
 
            types: 'users'
 
          },
 
          function(data) {
 
            callback(data.results)
 
          }
 
        );
 
      },
 
      sorter: function(query, items, searchKey) {
 
        return items;
 
      }
 
    },
 
    displayTpl: function(item) {
 
        return "<li>" +
 
            autocompleteGravatar(
 
                "{0} {1} ({2})".format(item.fname, item.lname, item.nname).html_escape(),
 
                '${gravatar_lnk}', 16) +
 
            "</li>";
 
    },
 
    insertTpl: "${atwho-at}${nname}"
 
  });
 
};
 

	
 

	
 
// Set caret at the given position in the input element
 
function _setCaretPosition($inputElement, caretPos) {
 
    $inputElement.each(function(){
 
        if(this.createTextRange) { // IE
 
            var range = this.createTextRange();
 
            range.move('character', caretPos);
 
            range.select();
 
        }
 
        else if(this.selectionStart) { // other recent browsers
 
            this.focus();
 
            this.setSelectionRange(caretPos, caretPos);
 
        }
 
        else // last resort - very old browser
 
            this.focus();
 
    });
 
}
 

	
 

	
 
var addReviewMember = function(id,fname,lname,nname,gravatar_link,gravatar_size){
 
    var displayname = nname;
 
    if ((fname != "") && (lname != "")) {
 
        displayname = "{0} {1} ({2})".format(fname, lname, nname);
 
    }
 
    var gravatarelm = gravatar(gravatar_link, gravatar_size, "");
 
    // WARNING: the HTML below is duplicate with
 
    // kallithea/templates/pullrequests/pullrequest_show.html
 
    // If you change something here it should be reflected in the template too.
 
    var element = (
 
        '     <li id="reviewer_{2}">\n'+
 
        '       <span class="reviewers_member">\n'+
 
        '         <input type="hidden" value="{2}" name="review_members" />\n'+
 
        '         <span class="reviewer_status" data-toggle="tooltip" title="not_reviewed">\n'+
 
        '             <i class="icon-circle changeset-status-not_reviewed"></i>\n'+
 
        '         </span>\n'+
 
        (gravatarelm ?
 
        '         {0}\n' :
 
        '')+
 
        '         <span>{1}</span>\n'+
 
        '         <a href="#" class="reviewer_member_remove" onclick="removeReviewMember({2})">\n'+
 
        '             <i class="icon-minus-circled"></i>\n'+
 
        '         </a> (add not saved)\n'+
 
        '       </span>\n'+
 
        '     </li>\n'
 
        ).format(gravatarelm, displayname.html_escape(), id);
 
    // check if we don't have this ID already in
 
    var ids = [];
 
    $('#review_members').find('li').each(function() {
 
            ids.push(this.id);
 
        });
 
    if(ids.indexOf('reviewer_'+id) == -1){
 
        //only add if it's not there
 
        $('#review_members').append(element);
 
    }
 
}
 

	
 
var removeReviewMember = function(reviewer_id, repo_name, pull_request_id){
 
    var $li = $('#reviewer_{0}'.format(reviewer_id));
 
    $li.find('div div').css("text-decoration", "line-through");
 
    $li.find('input').prop('name', 'review_members_removed');
 
    $li.find('.reviewer_member_remove').replaceWith('&nbsp;(remove not saved)');
 
}
 

	
 
/* activate auto completion of users as PR reviewers */
 
var PullRequestAutoComplete = function ($inputElement) {
 
    $inputElement.select2(
 
    {
 
        placeholder: $inputElement.attr('placeholder'),
 
        minimumInputLength: 1,
 
        ajax: {
 
            url: pyroutes.url('users_and_groups_data'),
 
            dataType: 'json',
 
            data: function(term, page){
 
              return {
 
                query: term
 
              };
 
            },
 
            results: function (data, page){
 
              return data;
 
            },
 
            cache: true
 
        },
 
        formatSelection: autocompleteFormatter,
 
        formatResult: autocompleteFormatter,
 
    }).on("select2-selecting", function(e) {
 
        addReviewMember(e.choice.id, e.choice.fname, e.choice.lname, e.choice.nname,
 
                        e.choice.gravatar_lnk, e.choice.gravatar_size);
 
        $inputElement.select2("close");
 
        e.preventDefault();
 
    });
 
}
 

	
 

	
 
function addPermAction(perm_type) {
 
    var template =
 
        '<td><input type="radio" value="{1}.none" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
 
        '<td><input type="radio" value="{1}.read" checked="checked" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
 
        '<td><input type="radio" value="{1}.write" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
 
        '<td><input type="radio" value="{1}.admin" name="perm_new_member_{0}" id="perm_new_member_{0}"></td>' +
 
        '<td>' +
 
                '<input class="form-control" id="perm_new_member_name_{0}" name="perm_new_member_name_{0}" value="" type="text" placeholder="{2}">' +
 
                '<input id="perm_new_member_type_{0}" name="perm_new_member_type_{0}" value="" type="hidden">' +
 
        '</td>' +
 
        '<td></td>';
 
    var $last_node = $('.last_new_member').last(); // empty tr between last and add
 
    var next_id = $('.new_members').length;
 
    $last_node.before($('<tr class="new_members">').append(template.format(next_id, perm_type, _TM['Type name of user or member to grant permission'])));
 
    MembersAutoComplete($("#perm_new_member_name_"+next_id), $("#perm_new_member_type_"+next_id));
 
}
 

	
 
function ajaxActionRevokePermission(url, obj_id, obj_type, field_id, extra_data) {
 
    var success = function (o) {
 
            $('#' + field_id).remove();
 
        };
 
    var failure = function (o) {
 
            alert(_TM['Failed to revoke permission'] + ": " + o.status);
 
        };
 
    var query_params = {};
 
    // put extra data into POST
 
    if (extra_data !== undefined && (typeof extra_data === 'object')){
 
        for(var k in extra_data){
 
            query_params[k] = extra_data[k];
 
        }
 
    }
 

	
 
    if (obj_type=='user'){
 
        query_params['user_id'] = obj_id;
 
        query_params['obj_type'] = 'user';
 
    }
 
    else if (obj_type=='user_group'){
 
        query_params['user_group_id'] = obj_id;
 
        query_params['obj_type'] = 'user_group';
 
    }
 

	
 
    ajaxPOST(url, query_params, success, failure);
 
};
 

	
 
/* Multi selectors */
 

	
 
var MultiSelectWidget = function(selected_id, available_id, form_id){
 
    var $availableselect = $('#' + available_id);
 
    var $selectedselect = $('#' + selected_id);
 

	
 
    //fill available only with those not in selected
 
    var $selectedoptions = $selectedselect.children('option');
 
    $availableselect.children('option').filter(function(i, e){
 
            for(var j = 0, node; node = $selectedoptions[j]; j++){
 
                if(node.value == e.value){
 
                    return true;
 
                }
 
            }
 
            return false;
 
        }).remove();
 

	
 
    $('#add_element').click(function(e){
 
            $selectedselect.append($availableselect.children('option:selected'));
 
        });
 
    $('#remove_element').click(function(e){
 
            $availableselect.append($selectedselect.children('option:selected'));
 
        });
 

	
 
    $('#'+form_id).submit(function(){
 
            $selectedselect.children('option').each(function(i, e){
 
                e.selected = 'selected';
 
            });
 
        });
 
}
 

	
 

	
 
/**
 
 Branch Sorting callback for select2, modifying the filtered result so prefix
 
 matches come before matches in the line.
 
 **/
 
var branchSort = function(results, container, query) {
 
    if (query.term) {
 
        return results.sort(function (a, b) {
 
            // Put closed branches after open ones (a bit of a hack ...)
 
            var aClosed = a.text.indexOf("(closed)") > -1,
 
                bClosed = b.text.indexOf("(closed)") > -1;
 
            if (aClosed && !bClosed) {
 
                return 1;
 
            }
 
            if (bClosed && !aClosed) {
 
                return -1;
 
            }
 

	
 
            // Put early (especially prefix) matches before later matches
 
            var aPos = a.text.toLowerCase().indexOf(query.term.toLowerCase()),
 
                bPos = b.text.toLowerCase().indexOf(query.term.toLowerCase());
 
            if (aPos < bPos) {
 
                return -1;
 
            }
 
            if (bPos < aPos) {
 
                return 1;
 
            }
 

	
 
            // Default sorting
 
            if (a.text > b.text) {
 
                return 1;
 
            }
 
            if (a.text < b.text) {
 
                return -1;
 
            }
 
            return 0;
 
        });
 
    }
 
    return results;
 
};
 

	
 
var prefixFirstSort = function(results, container, query) {
 
    if (query.term) {
 
        return results.sort(function (a, b) {
 
            // if parent node, no sorting
 
            if (a.children != undefined || b.children != undefined) {
 
                return 0;
 
            }
 

	
 
            // Put prefix matches before matches in the line
 
            var aPos = a.text.toLowerCase().indexOf(query.term.toLowerCase()),
 
                bPos = b.text.toLowerCase().indexOf(query.term.toLowerCase());
 
            if (aPos === 0 && bPos !== 0) {
 
                return -1;
 
            }
 
            if (bPos === 0 && aPos !== 0) {
 
                return 1;
 
            }
 

	
 
            // Default sorting
 
            if (a.text > b.text) {
 
                return 1;
 
            }
 
            if (a.text < b.text) {
 
                return -1;
 
            }
 
            return 0;
 
        });
 
    }
 
    return results;
 
};
 

	
 
/* Helper for jQuery DataTables */
 

	
 
var updateRowCountCallback = function updateRowCountCallback($elem, onlyDisplayed) {
 
    return function drawCallback() {
 
        var info = this.api().page.info(),
 
            count = onlyDisplayed === true ? info.recordsDisplay : info.recordsTotal;
 
        $elem.html(count);
 
    }
 
};
 

	
 

	
 
/**
 
 * activate changeset parent/child navigation links
 
 */
 
var activate_parent_child_links = function(){
 

	
 
    $('.parent-child-link').on('click', function(e){
 
        var $this = $(this);
 
        //fetch via ajax what is going to be the next link, if we have
 
        //>1 links show them to user to choose
 
        if(!$this.hasClass('disabled')){
 
            $.ajax({
 
                url: $this.data('ajax-url'),
 
                success: function(data) {
 
                    var repo_name = $this.data('reponame');
 
                    if(data.results.length === 0){
 
                        $this.addClass('disabled');
 
                        $this.text(_TM['No revisions']);
 
                    }
 
                    if(data.results.length === 1){
 
                        var commit = data.results[0];
 
                        window.location = pyroutes.url('changeset_home', {'repo_name': repo_name, 'revision': commit.raw_id});
 
                    }
 
                    else if(data.results.length > 1){
 
                        $this.addClass('disabled');
 
                        $this.addClass('double');
 
                        var template =
 
                            ($this.data('linktype') == 'parent' ? '<i class="icon-left-open"/> ' : '') +
 
                            '<a title="__title__" href="__url__">__rev__</a>' +
 
                            ($this.data('linktype') == 'child' ? ' <i class="icon-right-open"/>' : '');
 
                        var _html = [];
 
                        for(var i = 0; i < data.results.length; i++){
 
                            _html.push(template
 
                                .replace('__rev__', 'r{0}:{1}'.format(data.results[i].revision, data.results[i].raw_id.substr(0, 6)))
 
                                .replace('__title__', data.results[i].message)
 
                                .replace('__title__', data.results[i].message.html_escape())
 
                                .replace('__url__', pyroutes.url('changeset_home', {
 
                                    'repo_name': repo_name,
 
                                    'revision': data.results[i].raw_id}))
 
                                );
 
                        }
 
                        $this.html(_html.join('<br/>'));
 
                    }
 
                }
 
            });
 
        e.preventDefault();
 
        }
 
    });
 
}
0 comments (0 inline, 0 general)