從 WTForm 的 URLXSS 談開源組件的安全性

開源組件是咱們你們平時開發的時候必不可少的工具,所謂『不要重複造輪子』的緣由也是由於,大量封裝好的組件咱們在開發中能夠直接調用,減小了重複開發的工做量。
開源組件和開源程序也有一些區別,開源組件面向的使用者是開發者,而開源程序就能夠直接面向用戶。開源組件,如JavaScript裏的uploadify,php裏的PHPExcel等;開源程序,如php寫的wordpress、joomla,node.js寫的ghost等。
就安全而言,毋庸置疑,開源組件的漏洞影響面遠比開源軟件要大。但大量開源組件的漏洞卻不多出如今咱們眼中,我總結了幾條緣由:javascript

  1. 開源程序的漏洞具備通用性,不少能夠經過一個通用的poc來測試全網,更具『商業價值』;而開源組件因爲開發者使用方法不一樣,致使測試方法不統一,利用門檻也相對較高php

  2. 大衆更熟悉開源軟件,如wordpress,而不多有人知道wordpress內部使用了哪些開源組件。相應的,當出現漏洞的時候人們也只會認爲這個漏洞是wordpress的漏洞。html

  3. 慣性思惟讓人們認爲:『庫』裏應該不會有漏洞,在代碼審計的時候也不多會關注import進來的第三方庫的代碼缺陷。因此,開源組件爆出的漏洞也較少。java

  4. 可以開發開源組件的開發者自己素質相對較高,代碼質量較高,也使開源組件出漏洞的可能性較小。node

  5. 組件漏洞多半有爭議性,不少鍋分不清是組件自身的仍是其使用者的,不少問題咱們也只能稱其爲『特性』,但實際上這些特性反而比某些漏洞更可怕。python

特別是如今國內浮躁的安全氛圍,能夠明顯感覺到第一條緣由。就前段時間出現的幾個影響較大的漏洞:Java反序列化漏洞、joomla的代碼執行、redis的寫ssh key,能夠明顯感受到後二者炒的比前者要響,而前者不慍不火的,曝光了近一年才受到普遍關注。
Java反序列化漏洞,剛好就是典型的『組件』特性形成的問題。早在2015年的1月28號,就有白帽子報告了利用Apache Commons Collections這個經常使用的Java庫來實現任意代碼執行的方法,但並無太多關注(原來國外也是這樣)。直到11月有人提出了用這個方法攻擊WebLogic、WebSphere、JBoss、Jenkins、OpenNMS等應用的時候,才被忽然炒起來。
這種對比明顯反應出『開源組件』和『開源應用』在安全漏洞關注度上的差距。
我我的在烏雲上發過幾個組件漏洞,從前年發的ThinkPHP框架注入,到後面的Tornado文件讀取,到slimphp的XXE,基本都是我本身在使用完這些組件後,對總體代碼作code review的時候發現的。
這篇文章以一個例子,簡單地談談如何對第三方庫進行code review,與如何正確使用第三方庫。git

WTForm中的弱validator

WTForms是python web開發中重要的一個組件,它提供了簡單的表單生成、驗證、轉換等功能,是衆多python web框架(特別是flask)不可缺乏的輔助庫之一。
WTForms中有一個重要的功能就是對用戶輸入進行檢查,在文檔中被稱爲validator:
http://wtforms.readthedocs.org/en/latest/validators.htmlgithub

A validator simply takes an input, verifies it fulfills some criterion, such as a maximum length for a string and returns. Or, if the validation fails, raises a ValidationError. This system is very simple and flexible, and allows you to chain any number of validators on fields.web

咱們能夠簡單地使用其內置validator對數據進行檢查,好比咱們須要用戶輸入一個『不爲空』、『最短10個字符』、『最長64個字符』的『URL地址』,那麼咱們就能夠編寫以下class:redis

class MyForm(Form):
    url = StringField("Link", validators=[DataRequired(), Length(min=10, max=64), URL()])

以flask爲例,在view視圖中只需調用validate()函數便可檢查用戶的輸入是否合法:

@app.route('/', methods=['POST'])
def check():
    form = MyForm(flask.request.form)
    if form.validate():
        pass # right input
    else:
        pass # bad input

典型的敏捷開發手段,減小了大量開發工做量。
但我本身在作code review的過程當中發現,WTForms的內置validators並不可信,與其說是不可信,不如說在安全性上部分validator徹底不起任何做用。
就拿上訴代碼爲例子,這段代碼真的能夠檢查用戶輸入的數據是不是一個『URL』麼?咱們看到wtforms.validators.URL()類:

class URL(Regexp):
    """
    Simple regexp based url validation. Much like the email validator, you
    probably want to validate the url later by other means if the url must
    resolve.

    :param require_tld:
        If true, then the domain-name portion of the URL must contain a .tld
        suffix.  Set this to false if you want to allow domains like
        `localhost`.
    :param message:
        Error message to raise in case of a validation error.
    """
    def __init__(self, require_tld=True, message=None):
        regex = r'^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$'
        super(URL, self).__init__(regex, re.IGNORECASE, message)
        self.validate_hostname = HostnameValidation(
            require_tld=require_tld,
            allow_ip=True,
        )

    def __call__(self, form, field):
        message = self.message
        if message is None:
            message = field.gettext('Invalid URL.')

        match = super(URL, self).__call__(form, field, message)
        if not self.validate_hostname(match.group('host')):
            raise ValidationError(message)

其繼承了Rexexp類,實際上就是對用戶輸入進行正則匹配。咱們看到它的正則:

regex = r'^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$'

可見,這個正則與開發者理解的URL嚴重的不匹配。大部分的開發者但願得到的URL是一個『HTTP網址』,但這個正則匹配到的卻寬泛的太多了,最大特色就是其可匹配任意protocol。
最容易想到的一個攻擊方式就是利用Javascript協議觸發的XSS,好比我傳入的url是

javascript://...xss code

WTForms將認爲這是一個合法的URL,並存入數據庫。而在業務邏輯中URL一般是輸出在超連接的href屬性中,而href屬性支持利用Javascript僞協議執行JavaScript代碼。那麼,這裏就有極大的可能構造一個XSS攻擊。
另外一個草草編寫的validator是wtforms.validators.Email()類,查看其代碼:

class Email(Regexp):
    """
    Validates an email address. Note that this uses a very primitive regular
    expression and should only be used in instances where you later verify by
    other means, such as email activation or lookups.

    :param message:
        Error message to raise in case of a validation error.
    """
    def __init__(self, message=None):
        self.validate_hostname = HostnameValidation(
            require_tld=True,
        )
        super(Email, self).__init__(r'^.+@([^.@][^@]+)$', re.IGNORECASE, message)

    def __call__(self, form, field):
        message = self.message
        if message is None:
            message = field.gettext('Invalid email address.')

        match = super(Email, self).__call__(form, field, message)
        if not self.validate_hostname(match.group(1)):
            raise ValidationError(message)

看看他的正則^.+@([^.@][^@]+)$,這個正則根本沒法檢測用戶的輸入是不是Email。最前面的.+就讓一切壞字符全進入了數據庫。
因此我私下稱URL()和Email()爲URL Finder和Email Finder,而非validator,由於他們根本沒法驗證用戶輸入,卻是更適合做爲爬蟲查找目標的finder。

利用弱validator構造XSS

這個漏洞其實是出如今我寫的某個網站中。這個網站容許訪客輸入其博客地址,然後臺使用URL()對地址的合法性進行驗證,在用戶主頁其餘用戶能夠點擊其頭像訪問博客。
整個過程以下: https://gist.github.com/phith0n/807869afbe1365015627

#(๑¯ω¯๑) coding:utf8 (๑¯ω¯๑)
import os
import flask
from flask import Flask
from wtforms.form import Form
from wtforms.validators import DataRequired, URL
from wtforms import StringField
app = Flask(__name__)

class UrlForm(Form):
    url = StringField("Link", validators=[DataRequired(), URL()])

@app.route('/', methods=['GET', 'POST'])
def show_data():
    form = UrlForm(flask.request.form)
    if flask.request.method == "POST" and form.validate():
        url = form.url.data
    else:
        url = flask.request.url
    return flask.render_template('form.html', url=url, form=form)

if __name__ == '__main__':
    app.debug = False
    app.run(os.getenv('IP', '0.0.0.0'), int(os.getenv('PORT', 8080)))
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>test</title>
    </head>
    <body>
        <p>{% if form.url.errors %}
            {{ form.url.errors|join(' ') }}
           {% endif %}
        </p>
        <p>
            your input url
            <a href="{{ url }}" target="_blank">{{ url }}</a>
        </p>
        <form method="post">
            <input type="text" name="url" style="width:300px;" />
            <input type="submit" value="Submit"/>
        </form>
    </body>
</html>

demo頁面: https://flask-form-phith0n.c9users.io/ 可供測試。
那麼,這段代碼存在漏洞嗎?回顧URL的正則:

regex = r'^[a-z]+://(?P<host>[^/:]+)(?P<port>:[0-9]+)?(?P<path>\/.*)?$'
super(URL, self).__init__(regex, re.IGNORECASE, message)

有個//,實在討厭,將後面的內容所有註釋掉了,致使我不能直接執行JavaScript。繞過方法也簡單,由於//是單行註釋,因此只需換行便可。
但這裏正則修飾符是re.IGNORECASE,並無re.S,這就致使一旦出現換行這個正則將再也不匹配。
不過這個問題很快也有了答案,在JavaScript中,能夠表明換行的字符有\n \r \u2028和\u2029,而在正則裏換行僅僅是\n \r,因此我只要經過\u2028或\u2029這兩個字符代替換行便可。(\u2028的url編碼爲%E2%80%A8)
因此,傳入url以下便可:

javascript://www.baidu.com/
alert(1)

輸入以上url,提交後點擊連接便可觸發:

這個漏洞很典型,任何開發者都不會想到如此平凡的一段代碼居然隱藏着深層次的威脅。
有些人可能會以爲我這個demo並不能說明實際問題,我簡單翻了一下github,不到5分鐘就找到了一個存在一樣問題的項目: https://github.com/1jingdian/1jingdian 。(雖然其站點已經關閉,但代碼能夠瀏覽)

https://github.com/1jingdian/1jingdian/blob/master/application/forms/user.py

class SettingsForm(Form):
    motto = StringField('座右銘')
    blog = StringField('博客', validators=[Optional(), URL(message='連接格式不正確')])
    weibo = StringField('微博', validators=[Optional(), URL(message='連接格式不正確')])
    douban = StringField('豆瓣', validators=[Optional(), URL(message='連接格式不正確')])
    zhihu = StringField('知乎', validators=[Optional(), URL(message='連接格式不正確')])

這裏4個連接,全是用URL()來進行驗證。validate()經過後存入數據庫。
以後在我的頁面,提取出用戶信息傳入模板user/profile.html
https://github.com/1jingdian/1jingdian/blob/master/application/controllers/user.py#L14

def profile(uid, page):
    user = User.query.get_or_404(uid)
    votes = user.voted_pieces.paginate(page, 20)
    return render_template('user/profile.html', user=user, votes=votes)

跟進一下profile.html
https://github.com/1jingdian/1jingdian/blob/master/application/templates/user/profile.html

{% from "macros/_user.html" import render_user_profile_header %}
...
{{ render_user_profile_header(user, active="votes") }}

調用了marco,傳入render_user_profile_header函數,繼續跟進:
https://github.com/1jingdian/1jingdian/blob/master/application/templates/macros/_user.html#L37

{% macro render_user_profile_header(user, active="creates") %}
   ...
         <div class="media-icons">
            {% if user.blog %}
               <a href="{{ user.blog }}" target="_blank" title="博客">
                  <img src="{{ static('image/media/blog.png') }}" alt=""/>
               </a>
            {% endif %}
            {% if user.weibo %}
               <a href="{{ user.weibo }}" target="_blank" title="微博">
                  <img src="{{ static('image/media/weibo.jpg') }}" alt=""/>
               </a>
            {% endif %}
            {% if user.douban %}
               <a href="{{ user.douban }}" target="_blank" title="豆瓣">
                  <img src="{{ static('image/media/douban.png') }}" alt=""/>
               </a>
            {% endif %}
         </div>
      </div>

這裏將user.blog、user.weibo、user.douban都放入了a標籤的href屬性。這一系列操做實際上就是我以前那個demo的縮影,最終致使傳入的url過濾不嚴產生XSS。

開源組件漏洞究竟是誰的鍋?

這是多次受到爭議的話題之一,不少人認爲開源組件之因此形成了漏洞,都是由於開發者不規範使用組件致使的。
我以爲認定一個問題是開源組件的鍋,那麼必須知足如下條件:

  • 開發者按照文檔常規的方法進行開發

  • 文檔並無說明如此開發會存在什麼安全問題

  • 一樣的開發方式在其餘同類組件中沒有漏洞,而在該組件中產生漏洞

舉幾個例子,這個漏洞: http://www.wooyun.org/bugs/wooyun-2015-0101728 。首先知足第一個條件,正常使用S函數。固然文檔中也對安全進行了說明:

但這個說明,我以爲是不夠的。你『能夠』設置..參數,避免緩存文件名『被猜想到』。文檔並無說明緩存文件名被猜想到有什麼危害,也沒有強制要求設置這個參數。因此這個鍋,官方至少背一半。
再舉個例子: http://www.wooyun.org/bugs/wooyun-2010-0156208 ,很明顯的一個框架鍋,開發者在正常接收POST參數的時候就能夠形成XXE漏洞,這個漏洞和開發者是沒有任何關係的。
另外一個例子: http://www.wooyun.org/bugs/wooyun-2010-086742 ,咱們經過修改邏輯運算符改變開發者正常的判斷流程,形成安全問題。咱們對比一下ThinkPHP和Codeigniter,CI中對於邏輯運算符的位置就和TP不相同,它在『key』的位置:

正常狀況下key位置是不會被用戶控制的。因此,一樣的開發方式在CI裏不存在問題,而在TP裏就存在問題,這樣的地方我認爲也是ThinkPHP的鍋。
咱們看本文提出的WTForm的問題,這個鍋其實WTForm能夠不用獨自背。咱們在文檔中,能夠看到它有模模糊糊地提到過validater不嚴謹的問題:

固然,這個模糊的提示對於不少沒有安全基礎的人來講,很難起到做用。

開發者如何應對潛在的組件『安全特性』

那麼,沒有安全基礎的開發者,如何去應對潛在的組件安全特性。首先,我以爲常常作code review是頗有必要的,我會常常把本身寫的代碼也當作一個開源應用進行閱讀與審計,此時會常常發現一些以前沒注意到過的安全問題。code review的過程當中,要深刻地跟進一下第三方庫的源代碼,而不能僅僅是看本身寫的代碼,這樣才能發現一些潛在的特性。這些特性每每是形成漏洞的罪魁禍首。另外,文檔的閱讀能力也是極其重要的一點。其實大量的『框架特性』,框架文檔中都有必定的說明。不少開發者更喜歡去看example,以爲看代碼比看文字(也許與英文閱讀能力也有關係)更直觀,而不肯詳細閱讀說明。這種作法實際上在安全上是很是危險的,由於示例代碼一般都是官方給出的最簡陋的代碼,可能會忽略不少必要的安全措施。另外,具有必定的安全基礎是每一個開發必要的素質,緣由沒必要贅述。

相關文章
相關標籤/搜索