CVE-2014-2235: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site scripting (XSS) vulnerability in Askbot before 0.7.49 allows remote attackers to inject arbitrary web script or HTML via vectors related to the question search form.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Published: 2014-03-05T16:37Z
Repository: https://github.com/ASKBOT/askbot-devel
Language: HTML
Files changed: askbot/doc/source/changelog.rst, askbot/media/js/live_search.js, askbot/media/style/style.css, askbot/media/style/style.less, askbot/templates/meta/bottom_scripts.html, askbot/templates/tags.html, askbot/templates/tags/content.html, askbot/templates/tags/header.html, askbot/templates/widgets/search_bar.html, askbot/views/readers.py
Commit date: 2013-03-31 03:27:43 -0400
Commit message: added instant search to the tags page

Diff: diff --git a/askbot/doc/source/changelog.rst b/askbot/doc/source/changelog.rst
index f59138c5..7b65ee07 100644
--- a/askbot/doc/source/changelog.rst
+++ b/askbot/doc/source/changelog.rst
@@ -1,10 +1,11 @@
 Changes in Askbot
 =================
 
 Development version
 -------------------
+* Added instant search to the tags page
 * Added a placeholder template for the custom javascript on the question page
 * Allowed to disable the big "ask" button.
 * Some support for the media compression (Tyler Mandry)
 * Allowed to enable and disable question scopes on the main page
 * Added full text support for some languages with Postgresql:
diff --git a/askbot/media/js/live_search.js b/askbot/media/js/live_search.js
index dfae8321..15180260 100644
--- a/askbot/media/js/live_search.js
+++ b/askbot/media/js/live_search.js
@@ -265,10 +265,11 @@ TagWarningBox.prototype.showWarning = function(){
  * @constructor
  * tool tip to be shown on top of the search input
  */
 var InputToolTip = function() {
     WrappedElement.call(this);
+    this._promptText = gettext('search or ask your question');
 };
 inherits(InputToolTip, WrappedElement);
 
 InputToolTip.prototype.show = function() {
     this._element.removeClass('dimmed');
@@ -282,19 +283,23 @@ InputToolTip.prototype.hide = function() {
 
 InputToolTip.prototype.dim = function() {
     this._element.addClass('dimmed');
 };
 
+InputToolTip.prototype.setPromptText = function(text) {
+    this._promptText = text;
+};
+
 InputToolTip.prototype.setClickHandler = function(handler) {
     this._clickHandler = handler;
 };
 
 InputToolTip.prototype.createDom = function() {
     var element = this.makeElement('div');
     this._element = element;
     element.addClass('input-tool-tip');
-    element.html(gettext('search or ask your question'));
+    element.html(this._promptText);
     this.decorate(element);
 };
 
 InputToolTip.prototype.decorate = function(element) {
     this._element = element;
@@ -880,5 +885,141 @@ FullTextSearch.prototype.decorate = function(element) {
 
     this.activateTagSearchInput();
 
     $("form#searchForm").submit(me.makeFormSubmitHandler());
 };
+
+/**
+ * @constructor
+ */
+var TagSearch = function() {
+    WrappedElement.call(this);
+    this._isRunning = false;
+};
+inherits(TagSearch, WrappedElement);
+
+TagSearch.prototype.getQuery = function() {
+    return $.trim(this._element.val());
+};
+
+TagSearch.prototype.setQuery = function(val) {
+    this._element.val(val);
+};
+
+TagSearch.prototype.getSort = function() {
+    //todo: read it off the page
+    var link = $('.tabBar a.on');
+    if (link.length === 1) {
+        var sort = link.attr('id').replace('sort_', '');
+        if (sort === 'name' || sort === 'used') {
+            return sort;
+        }
+    }
+    return 'name';
+};
+
+TagSearch.prototype.getIsRunning = function() {
+    return this._isRunning;
+};
+
+TagSearch.prototype.setIsRunning = function(val) {
+    this._isRunning = val;
+};
+
+TagSearch.prototype.renderResult = function(html) {
+    this._contentBox.html(html);
+};
+
+TagSearch.prototype.runSearch = function() {
+    var data = {
+        'query': this.getQuery(),
+        'sort': this.getSort(),
+        'page': '1'
+    };
+    var me = this;
+    $.ajax({
+        dataType: 'json',
+        data: data,
+        cache: false,
+        url: askbot['urls']['tags'],
+        success: function(data) {
+            if (data['success']) {
+                me.renderResult(data['html']);
+                me.setIsRunning(false);
+            }
+        },
+        error: function() { me.setIsRunning(false); }
+    });
+    me.setIsRunning(true);
+};
+
+TagSearch.prototype.getToolTip = function() {
+    return this._toolTip;
+};
+
+TagSearch.prototype.makeKeyUpHandler = function() {
+    var me = this;
+    return function(evt) {
+        var keyCode = getKeyCode(evt);
+        if (me.getIsRunning() === false) {
+            me.runSearch();
+        }
+    };
+};
+
+TagSearch.prototype.makeKeyDownHandler = function() {
+    var me = this;
+    var xButton = this._xButton;
+    return function(evt) {
+        var query = me.getQuery();
+        var keyCode = getKeyCode(evt);
+        var toolTip = me.getToolTip();
+        if (keyCode === 27) {//escape
+            me.setQuery('');
+            toolTip.show();
+            xButton.hide();
+            return;
+        }
+        if (keyCode === 8 || keyCode === 48) {//del or backspace
+            if (query.length === 1) {
+                toolTip.show();
+                xButton.hide();
+            }
+        } else {
+            toolTip.hide();
+            xButton.show();
+        }
+    };
+};
+
+TagSearch.prototype.reset = function() {
+    if (this.getIsRunning() === false) {
+        this.setQuery('');
+        this._toolTip.show();
+        this._xButton.hide();
+        this.runSearch();
+        this._element.focus();
+    }
+};
+
+TagSearch.prototype.decorate = function(element) {
+    this._element = element;
+    this._contentBox = $('#ContentLeft');
+    this._xButton = $('input[name=reset_query]');
+    element.keyup(this.makeKeyUpHandler());
+    element.keydown(this.makeKeyDownHandler());
+
+    var me = this;
+    this._xButton.click(function(){ me.reset() });
+
+    var toolTip = new InputToolTip();
+    toolTip.setPromptText(askbot['data']['tagSearchPromptText']);
+    toolTip.setClickHandler(function() {
+        element.focus();
+    });
+    element.after(toolTip.getElement());
+    //below is called after getElement, b/c element must be defined
+    if (this.getQuery() !== '') {
+        toolTip.hide();//hide if search query is not empty
+    }
+    this._toolTip = toolTip;
+};
diff --git a/askbot/media/style/style.css b/askbot/media/style/style.css
index 23872136..fa4cf1b1 100644
--- a/askbot/media/style/style.css
+++ b/askbot/media/style/style.css
@@ -545,11 +545,10 @@ input[type="submit"].searchBtn {
   cursor: pointer;
   position: relative;
   z-index: 10001;
 }
 .groups-page input[type="submit"].searchBtn,
-.tags-page input[type="submit"].searchBtn,
 .badges-pages input[type="submit"].searchBtn,
 .user-profile-page input[type="submit"].searchBtn,
 .meta input[type="submit"].searchBtn,
 .openid-signin input[type="submit"].searchBtn,
 .users-page input[type="submit"].searchBtn {
diff --git a/askbot/media/style/style.less b/askbot/media/style/style.less
index 1976e2da..6a55aa2c 100644
--- a/askbot/media/style/style.less
+++ b/askbot/media/style/style.less
@@ -590,11 +590,10 @@ input[type="submit"].searchBtn {
     position: relative;
     z-index: 10001;
 }
 
 .groups-page, /* todo: clean up - should not need this */
-.tags-page,
 .badges-pages,
 .user-profile-page,
 .meta,
 .openid-signin,
 .users-page {
diff --git a/askbot/templates/meta/bottom_scripts.html b/askbot/templates/meta/bottom_scripts.html
index 805974cc..86a2490b 100644
--- a/askbot/templates/meta/bottom_scripts.html
+++ b/askbot/templates/meta/bottom_scripts.html
@@ -82,10 +82,13 @@
             search.setSearchUrl(askbot['data']['searchUrl']);
             if (activeTab === 'ask') {
                 search.setAskButtonEnabled(false);
             }
             search.decorate(searchInput);
+        } else if (activeTab === 'tags') {
+            var search = new TagSearch();
+            search.decorate(searchInput);
         }
 
         if (askbot['data']['userIsAdminOrMod']) {
             $('body').addClass('admin');
         }
diff --git a/askbot/templates/tags.html b/askbot/templates/tags.html
index e1608280..e2ab2c93 100644
--- a/askbot/templates/tags.html
+++ b/askbot/templates/tags.html
@@ -1,45 +1,10 @@
 {% extends "two_column_body.html" %}
-{% import "macros.html" as macros %}
 <!-- tags.html -->
 {% block title %}{% spaceless %}{% trans %}Tags{% endtrans %}{% endspaceless %}{% endblock %}
 {% block content %}
-<!-- Tabs -->
-{% include "tags/header.html" %}
-{% if tag_list_type == 'list' %}
-    {% if not tags.object_list %}
-        <span>{% trans %}Nothing found{% endtrans %}</span>
-    {% endif %}
-    {% if tags.object_list %}
-        <div class='clearfix'></div>
-        <ul class='tags'>
-            {% for tag in tags.object_list %}
-            <li>
-                {{ macros.tag_widget(
-                            tag=tag.name,
-                            html_tag='div',
-                            truncate_long_tag=True,
-                            extra_content='<span class="tag-number">&#215; ' ~
-                                            tag.used_count|intcomma ~ '</span>'
-                    )
-                }}
-            </li>
-            {% endfor %}
-        </ul>
-        <div class="clean"></div>
-        <div class="pager">
-            {{macros.paginator(paginator_context)}}
-        </div>
-    {% endif %}
-{% else %}
-    <div class="clearfix"></div>
-    {% if not tags %}
-        <span>{% trans %}Nothing found{% endtrans %}</span>
-    {% endif %}
-    {{ macros.tag_cloud(tags=tags, font_sizes=font_size, search_state=search_state) }}
-{% endif %}
-
+{% include "tags/content.html" %}
 {% endblock %}
 {% block endjs %}
     <script type="text/javascript">
     /*<![CDATA[*/
         $().ready(function(){
@@ -47,9 +12,11 @@
             $("#type-tag").attr('checked',true);
             Hilite.exact = false;
             Hilite.elementid = "searchtags";
             Hilite.debug_referrer = location.href;
         });
+        askbot['data']['tagSearchPromptText'] = "{% trans %}search for tags{% endtrans %}";
     /*]]>*/
     </script>
+    {% include "tags/custom_javascript.httml" ignore missing %}
 {% endblock %}
 <!-- end tags.html -->
diff --git a/askbot/templates/tags/content.html b/askbot/templates/tags/content.html
new file mode 100644
index 00000000..8567c274
--- /dev/null
+++ b/askbot/templates/tags/content.html
@@ -0,0 +1,34 @@
+{% import "macros.html" as macros %}
+{% include "tags/header.html" %}
+{% if tag_list_type == 'list' %}
+    {% if not tags.object_list %}
+        <span>{% trans %}Nothing found{% endtrans %}</span>
+    {% endif %}
+    {% if tags.object_list %}
+        <div class='clearfix'></div>
+        <ul class='tags'>
+            {% for tag in tags.object_list %}
+            <li>
+                {{ macros.tag_widget(
+                            tag=tag.name,
+                            html_tag='div',
+                            truncate_long_tag=True,
+                            extra_content='<span class="tag-number">&#215; ' ~
+                                            tag.used_count|intcomma ~ '</span>'
+                    )
+                }}
+            </li>
+            {% endfor %}
+        </ul>
+        <div class="clean"></div>
+        <div class="pager">
+            {{macros.paginator(paginator_context)}}
+        </div>
+    {% endif %}
+{% else %}
+    <div class="clearfix"></div>
+    {% if not tags %}
+        <span>{% trans %}Nothing found{% endtrans %}</span>
+    {% endif %}
+    {{ macros.tag_cloud(tags=tags, font_sizes=font_size, search_state=search_state) }}
+{% endif %}
diff --git a/askbot/templates/tags/header.html b/askbot/templates/tags/header.html
index 0832300e..37c16af8 100644
--- a/askbot/templates/tags/header.html
+++ b/askbot/templates/tags/header.html
@@ -1,27 +1,28 @@
 <div id="content-header">
+    {% set tag_query = stag|escape %}
     {% if page_title %}
             <h1 class="section-title">{{ page_title }}</h1>
     {% else %}
         {% if stag %}
-            <h1 class="section-title">{% trans %}Tags, matching "{{ stag }}"{% endtrans %}</h1>
+            <h1 class="section-title">{% trans %}Tags, matching "{{ tag_query }}"{% endtrans %}</h1>
         {% else %}
             <h1 class="section-title">{% trans %}Tags{% endtrans %}</h1>
         {% endif %}
     {% endif %}
     <div class="tabBar tabBar-tags">
         <div class="tabsA">
             <span class="label">{% trans %}Sort by &raquo;{% endtrans %}</span>
             <a
                 id="sort_name"
-                href="{% url tags %}?sort=name"
+                href="{% url tags %}?sort=name{% if tag_query %}&query={{ tag_query }}{% endif %}"
                 {% if tab_id == 'name' %}class="on"{% endif %}
                 title="{% trans %}sorted alphabetically{% endtrans %}"
             ><span>{% trans %}by name{% endtrans %}</span></a>
             <a
                 id="sort_used"
-                href="{% url tags %}?sort=used"
+                href="{% url tags %}?sort=used{% if tag_query %}&query={{ tag_query }}{% endif %}"
                 {% if tab_id == 'used' %}class="on"{% endif %}
                 title="{% trans %}sorted by frequency of tag use{% endtrans %}"
             ><span>{% trans %}by popularity{% endtrans %}</span></a>
             {% if settings.ENABLE_TAG_MODERATION 
                 and request.user.is_authenticated()
diff --git a/askbot/templates/widgets/search_bar.html b/askbot/templates/widgets/search_bar.html
index 5f0bb8de..1ec4ee81 100644
--- a/askbot/templates/widgets/search_bar.html
+++ b/askbot/templates/widgets/search_bar.html
@@ -5,18 +5,19 @@
     class="{% if query %}cancelable{% endif %}"
 >
     {# url action depends on which tab is active #}
     {% if active_tab == "tags" %}
         <input type="hidden" name="t" value="tag"/>
+        {% set query=stag %}
     {% elif active_tab == "users" %}
         <input type="hidden" name="t" value="user"/>
     {% endif %}
     <input 
         class="searchInput" 
         type="text" 
         autocomplete="off"
-        value="{{ query|default_if_none('') }}" 
+        value="{{ query|default_if_none('')|escape }}" 
         name="query" 
         id="keywords"
     />
     <input type="submit" value="" name="search" class="searchBtn" />
     <input type="button"
diff --git a/askbot/views/readers.py b/askbot/views/readers.py
index b0cf0eca..1d4594bb 100644
--- a/askbot/views/readers.py
+++ b/askbot/views/readers.py
@@ -243,28 +243,29 @@ def questions(request, **kwargs):
 
 
 def tags(request):#view showing a listing of available tags - plain list
 
     #1) Get parameters. This normally belongs to form cleaning.
-    sortby = request.GET.get('sort', 'used')
+    post_data = request.GET
+    sortby = post_data.get('sort', 'used')
     try:
-        page = int(request.GET.get('page', '1'))
+        page = int(post_data.get('page', '1'))
     except ValueError:
         page = 1
 
     if sortby == 'name':
         order_by = 'name'
     else:
         order_by = '-used_count'
 
-    query = request.GET.get('query', '').strip()
+    query = post_data.get('query', '').strip()
     tag_list_type = askbot_settings.TAG_LIST_FORMAT
 
     #2) Get query set for the tags.
     query_params = {'deleted': False}
     if query != '':
-        query_params['name__icontains': query]
+        query_params['name__icontains'] = query
 
     tags_qs = Tag.objects.filter(**query_params).exclude(used_count=0)
 
     tags_qs = tags_qs.order_by(order_by)
 
@@ -305,11 +306,18 @@ def tags(request):#view showing a listing of available tags - plain list
         font_size = extra_tags.get_tag_font_size(tags)
         data['font_size'] = font_size
 
     data['tags'] = tags
 
-    return render(request, 'tags.html', data)
+    if request.is_ajax():
+        template = get_template('tags/content.html')
+        template_context = RequestContext(request, data)
+        json_data = {'success': True, 'html': template.render(template_context)}
+        json_string = simplejson.dumps(json_data)
+        return HttpResponse(json_string, mimetype='application/json')
+    else:
+        return render(request, 'tags.html', data)
 
 @csrf.csrf_protect
 def question(request, id):#refactor - long subroutine. display question body, answers and comments
     """view that displays body of the question and
     all answers to it

Vulnerable code:     element.html(gettext('search or ask your question'));
.tags-page input[type="submit"].searchBtn,
.tags-page,
{% import "macros.html" as macros %}
<!-- Tabs -->
{% include "tags/header.html" %}
{% if tag_list_type == 'list' %}
    {% if not tags.object_list %}
        <span>{% trans %}Nothing found{% endtrans %}</span>
    {% endif %}
    {% if tags.object_list %}
        <div class='clearfix'></div>
        <ul class='tags'>
            {% for tag in tags.object_list %}
            <li>
                {{ macros.tag_widget(
                            tag=tag.name,
                            html_tag='div',
                            truncate_long_tag=True,
                            extra_content='<span class="tag-number">&#215; ' ~
                                            tag.used_count|intcomma ~ '</span>'
                    )
                }}
            </li>
            {% endfor %}
        </ul>
        <div class="clean"></div>
        <div class="pager">
            {{macros.paginator(paginator_context)}}
        </div>
    {% endif %}
{% else %}
    <div class="clearfix"></div>
    {% if not tags %}
        <span>{% trans %}Nothing found{% endtrans %}</span>
    {% endif %}
    {{ macros.tag_cloud(tags=tags, font_sizes=font_size, search_state=search_state) }}
{% endif %}

            <h1 class="section-title">{% trans %}Tags, matching "{{ stag }}"{% endtrans %}</h1>
                href="{% url tags %}?sort=name"
                href="{% url tags %}?sort=used"
        value="{{ query|default_if_none('') }}" 
    sortby = request.GET.get('sort', 'used')
        page = int(request.GET.get('page', '1'))
    query = request.GET.get('query', '').strip()
        query_params['name__icontains': query]
    return render(request, 'tags.html', data)
Fixed code: * Added instant search to the tags page
    this._promptText = gettext('search or ask your question');
InputToolTip.prototype.setPromptText = function(text) {
    this._promptText = text;
};

    element.html(this._promptText);

/**
 * @constructor
 */
var TagSearch = function() {
    WrappedElement.call(this);
    this._isRunning = false;
};
inherits(TagSearch, WrappedElement);

TagSearch.prototype.getQuery = function() {
    return $.trim(this._element.val());
};

TagSearch.prototype.setQuery = function(val) {
    this._element.val(val);
};

TagSearch.prototype.getSort = function() {
    //todo: read it off the page
    var link = $('.tabBar a.on');
    if (link.length === 1) {
        var sort = link.attr('id').replace('sort_', '');
        if (sort === 'name' || sort === 'used') {
            return sort;
        }
    }
    return 'name';
};

TagSearch.prototype.getIsRunning = function() {
    return this._isRunning;
};

TagSearch.prototype.setIsRunning = function(val) {
    this._isRunning = val;
};

TagSearch.prototype.renderResult = function(html) {
    this._contentBox.html(html);
};

TagSearch.prototype.runSearch = function() {
    var data = {
        'query': this.getQuery(),
        'sort': this.getSort(),
        'page': '1'
    };
    var me = this;
    $.ajax({
        dataType: 'json',
        data: data,
        cache: false,
        url: askbot['urls']['tags'],
        success: function(data) {
            if (data['success']) {
                me.renderResult(data['html']);
                me.setIsRunning(false);
            }
        },
        error: function() { me.setIsRunning(false); }
    });
    me.setIsRunning(true);
};

TagSearch.prototype.getToolTip = function() {
    return this._toolTip;
};

TagSearch.prototype.makeKeyUpHandler = function() {
    var me = this;
    return function(evt) {
        var keyCode = getKeyCode(evt);
        if (me.getIsRunning() === false) {
            me.runSearch();
        }
    };
};

TagSearch.prototype.makeKeyDownHandler = function() {
    var me = this;
    var xButton = this._xButton;
    return function(evt) {
        var query = me.getQuery();
        var keyCode = getKeyCode(evt);
        var toolTip = me.getToolTip();
        if (keyCode === 27) {//escape
            me.setQuery('');
            toolTip.show();
            xButton.hide();
            return;
        }
        if (keyCode === 8 || keyCode === 48) {//del or backspace
            if (query.length === 1) {
                toolTip.show();
                xButton.hide();
            }
        } else {
            toolTip.hide();
            xButton.show();
        }
    };
};

TagSearch.prototype.reset = function() {
    if (this.getIsRunning() === false) {
        this.setQuery('');
        this._toolTip.show();
        this._xButton.hide();
        this.runSearch();
        this._element.focus();
    }
};

TagSearch.prototype.decorate = function(element) {
    this._element = element;
    this._contentBox = $('#ContentLeft');
    this._xButton = $('input[name=reset_query]');
    element.keyup(this.makeKeyUpHandler());
    element.keydown(this.makeKeyDownHandler());

    var me = this;
    this._xButton.click(function(){ me.reset() });

    var toolTip = new InputToolTip();
    toolTip.setPromptText(askbot['data']['tagSearchPromptText']);
    toolTip.setClickHandler(function() {
        element.focus();
    });
    element.after(toolTip.getElement());
    //below is called after getElement, b/c element must be defined
    if (this.getQuery() !== '') {
        toolTip.hide();//hide if search query is not empty
    }
    this._toolTip = toolTip;
};
        } else if (activeTab === 'tags') {
            var search = new TagSearch();
            search.decorate(searchInput);
{% include "tags/content.html" %}
        askbot['data']['tagSearchPromptText'] = "{% trans %}search for tags{% endtrans %}";
    {% include "tags/custom_javascript.httml" ignore missing %}
{% import "macros.html" as macros %}
{% include "tags/header.html" %}
{% if tag_list_type == 'list' %}
    {% if not tags.object_list %}
        <span>{% trans %}Nothing found{% endtrans %}</span>
    {% endif %}
    {% if tags.object_list %}
        <div class='clearfix'></div>
        <ul class='tags'>
            {% for tag in tags.object_list %}
            <li>
                {{ macros.tag_widget(
                            tag=tag.name,
                            html_tag='div',
                            truncate_long_tag=True,
                            extra_content='<span class="tag-number">&#215; ' ~
                                            tag.used_count|intcomma ~ '</span>'
                    )
                }}
            </li>
            {% endfor %}
        </ul>
        <div class="clean"></div>
        <div class="pager">
            {{macros.paginator(paginator_context)}}
        </div>
    {% endif %}
{% else %}
    <div class="clearfix"></div>
    {% if not tags %}
        <span>{% trans %}Nothing found{% endtrans %}</span>
    {% endif %}
    {{ macros.tag_cloud(tags=tags, font_sizes=font_size, search_state=search_state) }}
{% endif %}
    {% set tag_query = stag|escape %}
            <h1 class="section-title">{% trans %}Tags, matching "{{ tag_query }}"{% endtrans %}</h1>
                href="{% url tags %}?sort=name{% if tag_query %}&query={{ tag_query }}{% endif %}"
                href="{% url tags %}?sort=used{% if tag_query %}&query={{ tag_query }}{% endif %}"
        {% set query=stag %}
        value="{{ query|default_if_none('')|escape }}" 
    post_data = request.GET
    sortby = post_data.get('sort', 'used')
        page = int(post_data.get('page', '1'))
    query = post_data.get('query', '').strip()
        query_params['name__icontains'] = query
    if request.is_ajax():
        template = get_template('tags/content.html')
        template_context = RequestContext(request, data)
        json_data = {'success': True, 'html': template.render(template_context)}
        json_string = simplejson.dumps(json_data)
        return HttpResponse(json_string, mimetype='application/json')
    else:
        return render(request, 'tags.html', data)