1. 程式人生 > Django入門教學 >31 Django 中的 Web 安全手段

31 Django 中的 Web 安全手段

今天我們來簡單聊一下在 Django 中針對常見的 Web 攻擊手段做了哪些必要的防護措施,以及接下來我們在 Django 專案開發中需要注意哪些安全知識,避免給專案挖坑。

1. 深入 Django 中 CSRF 校驗過程

上一節中提到了,針對 CSRF 攻擊有效的解決方案是在網頁上新增一個隨機的校驗 token 值,我們前面的登入的模板頁面中新增的 {% csrf_token %},這裡正好對應著一個隨機值。我們拿之前的登入表單來進行觀察,會發現這樣幾個現象:

  • 網頁上隱藏的 csrf_token 值會在每次重新整理時變化
  • 對應在請求和響應頭部的 cookie 中的 csrftoken值卻一直不變

圖片描述
圖片描述

這樣子我們對應會產生幾個思考問題:

  • 為什麼網頁上的 token 值會變,而 cookie 中的 token 則一直不變?
  • 整個 token 的校驗過程是怎樣的,有密碼?如果有密碼,密碼存在哪裡?

今天我們會帶著這兩個問題,檢視下 Django 內部原始碼,找到這些問題的程式碼位置。我可能不會很完整的描述整個程式碼執行的邏輯,因為篇幅不夠,而且細節太多,容易迷失在程式碼的海洋裡。首先毋庸置疑的第一步是找我們在 settings.py 中設定的 CSRF 中介軟體:

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware'
, 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware'
, ]

我們在上一講中提到過中介軟體類的兩個函式:process_request()process_response()。而在 CSRF 中介軟體檔案中還有一個方法:process_view()。中間類比較完整的處理流程示意圖如下所示,可以看到中介軟體的 process_view() 方法如果返回 None,則會執行下一個 中介軟體的 process_view() 方法。一旦它返回 HttpResponse 例項,則直接跳過檢視函式到達最後一箇中間件的 process_response() 方法中。

圖片描述

我們來關注下 django.middleware 目錄下的 csrf.py 檔案,所有的答案都在這裡可以找到。首先看最核心的中介軟體類:

# 原始碼位置:django/middleware/csrf.py

# ...

class CsrfViewMiddleware(MiddlewareMixin):
    def _accept(self, request):
        # Avoid checking the request twice by adding a custom attribute to
        # request.  This will be relevant when both decorator and middleware
        # are used.
        request.csrf_processing_done = True
        return None
    
    def _reject(self, request, reason):
        response = _get_failure_view()(request, reason=reason)
        log_response(
            'Forbidden (%s): %s', reason, request.path,
            response=response,
            request=request,
            logger=logger,
        )
        return response
    
    def _get_token(self, request):
        # ...
        
    def _set_token(self, request, response):
        # ...
    
    def process_request(self, request):
        csrf_token = self._get_token(request)
        if csrf_token is not None:
            # Use same token next time.
            request.META['CSRF_COOKIE'] = csrf_token
    
    def process_view(self, request, callback, callback_args, callback_kwargs):
        if getattr(request, 'csrf_processing_done', False):
            return None

        # Wait until request.META["CSRF_COOKIE"] has been manipulated before
        # bailing out, so that get_token still works
        if getattr(callback, 'csrf_exempt', False):
            return None

        # Assume that anything not defined as 'safe' by RFC7231 needs protection
        if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
            if getattr(request, '_dont_enforce_csrf_checks', False):
                # Mechanism to turn off CSRF checks for test suite.
                # It comes after the creation of CSRF cookies, so that
                # everything else continues to work exactly the same
                # (e.g. cookies are sent, etc.), but before any
                # branches that call reject().
                return self._accept(request)

            # 判斷是不是 https 協議,不然不用執行這裡
            if request.is_secure():
                # ...

            csrf_token = request.META.get('CSRF_COOKIE')
            if csrf_token is None:
                # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
                # and in this way we can avoid all CSRF attacks, including login
                # CSRF.
                return self._reject(request, REASON_NO_CSRF_COOKIE)

            # Check non-cookie token for match.
            request_csrf_token = ""
            if request.method == "POST":
                try:
                    request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
                except IOError:
                    # Handle a broken connection before we've completed reading
                    # the POST data. process_view shouldn't raise any
                    # exceptions, so we'll ignore and serve the user a 403
                    # (assuming they're still listening, which they probably
                    # aren't because of the error).
                    pass

            if request_csrf_token == "":
                # Fall back to X-CSRFToken, to make things easier for AJAX,
                # and possible for PUT/DELETE.
                request_csrf_token = request.META.get(settings.CSRF_HEADER_NAME, '')

            request_csrf_token = _sanitize_token(request_csrf_token)
            if not _compare_salted_tokens(request_csrf_token, csrf_token):
                return self._reject(request, REASON_BAD_TOKEN)

        return self._accept(request)
        
    def process_response(self, request, response):
        if not getattr(request, 'csrf_cookie_needs_reset', False):
            if getattr(response, 'csrf_cookie_set', False):
                return response

        if not request.META.get("CSRF_COOKIE_USED", False):
            return response

        # Set the CSRF cookie even if it's already set, so we renew
        # the expiry timer.
        self._set_token(request, response)
        response.csrf_cookie_set = True
        return response

這裡比較複雜的部分就是 process_view() 方法。process_request() 方法只是從請求頭中取出 csrftoken 值或者生成一個 csrftoken 值放到 request.META 屬性中去;process_response() 會設定對應的 csrftoken 值到 cookie 或者 session 中去。這裡獲取 csrftoken 和 設定 csrftoken 呼叫的正是 _get_token()set_token()方法:

class CsrfViewMiddleware(MiddlewareMixin):
    # ...
    def _get_token(self, request):
        if settings.CSRF_USE_SESSIONS:
            try:
                return request.session.get(CSRF_SESSION_KEY)
            except AttributeError:
                raise ImproperlyConfigured(
                    'CSRF_USE_SESSIONS is enabled, but request.session is not '
                    'set. SessionMiddleware must appear before CsrfViewMiddleware '
                    'in MIDDLEWARE%s.' % ('_CLASSES' if settings.MIDDLEWARE is None else '')
                )
        else:
            try:
                cookie_token = request.COOKIES[settings.CSRF_COOKIE_NAME]
            except KeyError:
                return None

            csrf_token = _sanitize_token(cookie_token)
            if csrf_token != cookie_token:
                # Cookie token needed to be replaced;
                # the cookie needs to be reset.
                request.csrf_cookie_needs_reset = True
            return csrf_token

    def _set_token(self, request, response):
        if settings.CSRF_USE_SESSIONS:
            if request.session.get(CSRF_SESSION_KEY) != request.META['CSRF_COOKIE']:
                request.session[CSRF_SESSION_KEY] = request.META['CSRF_COOKIE']
        else:
            response.set_cookie(
                settings.CSRF_COOKIE_NAME,
                request.META['CSRF_COOKIE'],
                max_age=settings.CSRF_COOKIE_AGE,
                domain=settings.CSRF_COOKIE_DOMAIN,
                path=settings.CSRF_COOKIE_PATH,
                secure=settings.CSRF_COOKIE_SECURE,
                httponly=settings.CSRF_COOKIE_HTTPONLY,
                samesite=settings.CSRF_COOKIE_SAMESITE,
            )
            # Set the Vary header since content varies with the CSRF cookie.
            patch_vary _headers(response, ('Cookie',))
    # ...

如果我們沒在 settings.py 中設定 CSRF_USE_SESSIONS 值時,在 django/conf/global_settings.py 預設設定為 False,那麼我們就是呼叫前面熟悉的 response.set_cookie() 方法取設定 cookie 中的 key-value 值,也是我們在上面第二張圖片所看到的 Set-Cookie 裡面的值。

我們來看最核心的處理方法:process_view()。它的執行流程如下所列,略有刪減,請仔細研讀和對照程式碼:

  • 判斷檢視方法是否有 csrf_exempt 屬性。相當於該檢視方法添加了 @csrf_exempt 裝飾器,這樣不用檢驗 csrf_token 值,直接返回 None,進入下面的中介軟體執行,直到檢視函式去處理 HTTP 請求;

  • 對於 GET、HEAD、 OPTIONS、 TRACE 這四種請求不用檢查 csrf_token,會直接跳到最後執行 self._accept(request) 方法。但是我們常用的如 POST、PUT 以及 DELETE 等請求會進行特別的處理;

來看針對 POST、PUT 以及 DELETE 的特殊處理,要注意兩處程式碼:

  • request_csrf_token 值的獲取:對於 POST 請求,我們要從請求引數中獲取,這個值正是表單中隱藏的隨機 csrf_token,也是我們在第一張圖中看到的值,每次請求都會重新整理該值;而且對於其它的請求,該值則是從 request.META 中獲取;

  • 校驗 csrf_token 值是否正確。如果是不正確的 csrf_token 值,則會直接返回 403 錯誤;

    if not _compare_salted_tokens(request_csrf_token, csrf_token):
        return self._reject(request, REASON_BAD_TOKEN)
    

    可以看到,這裡校驗的是兩個值:一個是我們從 cookie 中獲取的,另一個是前端表單中隱藏的那個隨機數。

現在我們大致心裡有個數了,Django 的校驗方法竟然是用 cookie 中的值和頁面上的隨機值進行校驗,這兩個值都是64位的,你必須同時拿到這兩個正確 token 值才能通過 Django 的 csrf 中介軟體校驗。

比較原理,2個 token,一個放到 cookie 中,另一個放到表單中,會一直變得那種。接下來就是對這兩個 token 進行對比。我們繼續追蹤 _compare_salted_tokens() 方法,可以在 csrf.py 中找到如下兩個方法,它們分別對應著 csrf_token 值的生成和解碼:

# 原始碼位置:django/middleware/csrf.py
# ...

def _salt_cipher_secret(secret):
    """
    Given a secret (assumed to be a string of CSRF_ALLOWED_CHARS), generate a
    token by adding a salt and using it to encrypt the secret.
    """
    salt = _get_new_csrf_string()
    chars = CSRF_ALLOWED_CHARS
    pairs = zip((chars.index(x) for x in secret), (chars.index(x) for x in salt))
    cipher = ''.join(chars[(x + y) % len(chars)] for x, y in pairs)
    return salt + cipher


def _unsalt_cipher_token(token):
    """
    Given a token (assumed to be a string of CSRF_ALLOWED_CHARS, of length
    CSRF_TOKEN_LENGTH, and that its first half is a salt), use it to decrypt
    the second half to produce the original secret.
    """
    salt = token[:CSRF_SECRET_LENGTH]
    token = token[CSRF_SECRET_LENGTH:]
    chars = CSRF_ALLOWED_CHARS
    pairs = zip((chars.index(x) for x in token), (chars.index(x) for x in salt))
    secret = ''.join(chars[x - y] for x, y in pairs)  # Note negative values are ok
    return secret

# ...

來看這兩個函式,首先是 _salt_cipher_secret() 方法,需要傳入一個長度為 32 的 secret,就可以得到一個64位的隨機字元。這個 secret 值在使用時也是隨機生成的32個字元:

# 原始碼位置:django/middleware/csrf.py
def _get_new_csrf_string():
    return get_random_string(CSRF_SECRET_LENGTH, allowed_chars=CSRF_ALLOWED_CHARS)

# 原始碼位置:django/utils/crypto.py
def get_random_string(length=12,
                      allowed_chars='abcdefghijklmnopqrstuvwxyz'
                                    'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'):
    """
    Return a securely generated random string.

    The default length of 12 with the a-z, A-Z, 0-9 character set returns
    a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
    """
    if not using_sysrandom:
        # This is ugly, and a hack, but it makes things better than
        # the alternative of predictability. This re-seeds the PRNG
        # using a value that is hard for an attacker to predict, every
        # time a random string is required. This may change the
        # properties of the chosen random sequence slightly, but this
        # is better than absolute predictability.
        random.seed(
            hashlib.sha256(
                ('%s%s%s' % (random.getstate(), time.time(), settings.SECRET_KEY)).encode()
            ).digest()
        )
    return ''.join(random.choice(allowed_chars) for i in range(length))

_salt_cipher_secret() 方法中我們可以看到,傳入32位的金鑰 secret,最後的 csrf_token 的生成是 salt + cipher,前32位是 salt,後32位是加密字串。解密的過程差不多就是 _salt_cipher_secret() 的逆過程了,最後得到 secret。我們可以在 Django 的 shell 模式下使用下這兩個函式:

(django-manual) [root@server first_django_app]# python manage.py shell
Python 3.8.1 (default, Dec 24 2019, 17:04:00) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.middleware.csrf import _get_new_csrf_token, _unsalt_cipher_token
>>> x1 = _get_new_csrf_token()
>>> x2 = _get_new_csrf_token()
>>> x3 = _get_new_csrf_token()
>>> print('x1={}\nx2={}\nx3={}'.format(x1, x2, x3))
x1=dvK3CRLiyHJ6Xgt0B6eZ7kUjxXgZ5CKkhl8HbHq8CKR0ZXMOxYnigzDTIZIdk3xZ
x2=TMazqRDst3BSiyxIAI1XDiFKdbmxu8nKRVvMogERiZi6IG6KNhDSxcgEOPTqU0qF
x3=gy998wPOCZJiXHo7HYQtY3dfwaevPHKAs2YXPAeJmWUaA5vV2xdXqvlidLR4XM1T
>>> _unsalt_cipher_token(x1)
'e0yOJ0P0edi4cRtY62jtjpTKlcCopBXP'
>>> _unsalt_cipher_token(x2)
'8jvn8zbzZ6RoAiJcnJM544L4LOH3A2d5'
>>> _unsalt_cipher_token(x3)
'mEZYRez5U7l2NyhYvJxECCidRLNJifrt'
>>> 

瞭解了上述這些方法後,現在來思考前面提出的問題:為什麼每次重新整理表單中的 csrf_token 值會一直變化,而 cookie 中的 csrf_token 值卻一直不變呢?首先我們看在頁面上生成隨機 token 值的程式碼,也就是將標籤 {{ csrf_token }} 轉成 64位隨機碼的地方:

# 原始碼位置: django/template/defaulttags.py

@register.tag
def csrf_token(parser, token):
    return CsrfTokenNode()

class CsrfTokenNode(Node):
    def render(self, context):
        csrf_token = context.get('csrf_token')
        if csrf_token:
            if csrf_token == 'NOTPROVIDED':
                return format_html("")
            else:
                return format_html('<input type="hidden" name="csrfmiddlewaretoken" value="{}">', csrf_token)
        else:
            # It's very probable that the token is missing because of
            # misconfiguration, so we raise a warning
            if settings.DEBUG:
                warnings.warn(
                    "A {% csrf_token %} was used in a template, but the context "
                    "did not provide the value.  This is usually caused by not "
                    "using RequestContext."
                )
            return ''

可以看到 csrf_token 值是從 context 中取出來的,而在 context 中的 csrf_token 值又是由如下程式碼生成的:

# 原始碼位置:django/template/context_processors.py

from django.middleware.csrf import get_token

# ...

def csrf(request):
    """
    Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if
    it has not been provided by either a view decorator or the middleware
    """
    def _get_val():
        token = get_token(request)
        if token is None:
            # In order to be able to provide debugging info in the
            # case of misconfiguration, we use a sentinel value
            # instead of returning an empty dict.
            return 'NOTPROVIDED'
        else:
            return token

    return {'csrf_token': SimpleLazyObject(_get_val)}

可以看到,最後 csrf_token 值還是由 csrf.py 檔案中的 get_token() 方法生成的。來繼續看這個 get_token() 方法的程式碼:

# 原始碼位置:django/middleware/csrf.py

def get_token(request):
    """
    Return the CSRF token required for a POST form. The token is an
    alphanumeric value. A new token is created if one is not already set.

    A side effect of calling this function is to make the csrf_protect
    decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
    header to the outgoing response.  For this reason, you may need to use this
    function lazily, as is done by the csrf context processor.
    """
    if "CSRF_COOKIE" not in request.META:
        csrf_secret = _get_new_csrf_string()
        request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
    else:
        csrf_secret = _unsalt_cipher_token(request.META["CSRF_COOKIE"])
    request.META["CSRF_COOKIE_USED"] = True
    return _salt_cipher_secret(csrf_secret)

注意注意!最關鍵的地方來了,這個加密的 secret 的值是從哪裡來的?正是從請求頭中的 cookie 資訊中來的,如果沒有將生成一個新的金鑰,接著把該金鑰生成的 token 放到 cookie 中。最後使用 _salt_cipher_secret() 方法生成的 csrf_token 和 cookie 中的 csrf_token 具有相同的金鑰。同時拿到了這兩個值,就可以進行校驗和判斷,下面我們在 ``_salt_cipher_secret()方法中加上一個print()` 語句,然後執行下看看是否如我們所說。

可以看到每次生成 token 時加密的祕鑰都是一樣的。我們從上面生成的 csrf_token 中選一個進行解密,得到的結果和 cookie 中的正是一樣的金鑰:

>>> from django.middleware.csrf import _unsalt_cipher_token
# 兩個 token 解密是相同的,這才是正確的
>>> _unsalt_cipher_token('2Tt8StiU4rZcvCrTb2KqJwTTOTCP0WvJhp7GyTj58RGv97IvJInxyrAN4DKCdt1M')
'pGOIQAbleARtOFrMIQNhZ5R4qUiXnHGd'
>>> _unsalt_cipher_token('VI68m6xT1JczSsnuJvxqtcr0L0EvCN1DaeKG2wy459TSwXE6hbaxi78U1KMiPkxG')
'pGOIQAbleARtOFrMIQNhZ5R4qUiXnHGd'

現在大部分程式碼我們也算清楚了,csrf_token 的校驗原理我們也知道了。那麼如果想自己生成 csrf_token 並通過 Django 的校驗也非常簡單,只需要通過那個金鑰生成一個 csrf_token 或者直接輸入使用金鑰都可以通過校驗。我們首先使用金鑰在 shell 模式下隨機生成一個 csrf_token 值:

>>> from django.middleware.csrf import _salt_cipher_secret
>>> _salt_cipher_secret('pGOIQAbleARtOFrMIQNhZ5R4qUiXnHGd')
'ObaC9DEZfn4seXbOhgBCph2Y5PMjm0Eo3HOaP3FajNLLSssqPWeJecJSlzU6zxar

接下來在看我的演示,第一此我隨機改動 csrf_token 的字元,這樣校驗肯定通不過;接下來我是用自己生成的 token 值以及直接填寫金鑰再去提交都能通過校驗。為了方便演示結果,我們在 csrf.pyprocess_view() 函式中新增幾個 print() 方法,方便我們理解執行的過程。

圖片描述

2. Django 中 XSS 漏洞防護

在 Django 中也提供了部分程式碼來幫助我們防止 XSS 漏洞,我們需要熟悉 Django 的相關程式碼才能使用好它。在模板檔案中,Django 使用 escape 過濾器對單一變數進行轉義過濾,無需轉義時使用 safe 過濾器;此外 Django 預設對 HTML 自動轉義,使用的標籤為:{% autoescape on %},而如想停止自動轉義,可以使用 off 引數關閉該標籤:{% autoescape off %}。對於 Django 做的這些網頁元素安全、防止 XSS 漏洞的工作的程式碼主要在 django/utils/html_safe.py 檔案中,如果有興趣可以深入學習下這裡的程式碼。但是有這些程式碼真的就萬無一失了嗎?這種想法是錯誤的,比如我們人為的用 safe 不對變數進行轉義,有時候控制不好就會造成漏洞,更多的時候,Django 給我們寫好了很多安全程式碼,但我們需要用好這些程式碼,同時也要加強安全相關的知識背景,儘量減少常見的漏洞出現。

3. Django 中對 SQL 注入漏洞做的工作

Django 內建的 ORM 模型某種程度上幫我們處理好了 SQL 注入問題,我們儘量使用 Django 內建 ORM 模型的 api 去對資料庫中的表進行增刪改查操作,它會根據我們所使用的資料庫伺服器的轉換規則,自動轉義特殊的SQL引數,從而避免出現 SQL 注入的問題。這個操作被運用到了整個 Django 的 ORM 模型的 api 中,但也有一些例外,如給 extra() 方法的 where 引數, 這個引數故意設計成可以接受原始的 SQL,並使用底層資料庫API的查詢。我們來看存在 SQL 注入漏洞和正確操作者兩種寫法:

# 存在SQL注入漏洞程式碼
name = 'Joe'  # 如果first_name中有SQL特定字元就會出現漏洞
User.objects.all().extra(where=["name='%s' and password='%s'" % (name, password)])
# 正確方式
User.objects.all().extra(where=["name='%s' and password='%s'"], params=[name, password])

我們前面在 ORM 操作中建立了一個 user 表,對應的 model 類如下:

# 程式碼位置: hello_app/models.py
class User(models.Model):
    name = models.CharField('使用者名稱', max_length=20)
    password = models.CharField('密碼', max_length=50)
    email = models.EmailField('郵箱')

    def __str__(self):
        return "<%s>" % (self.name)

    class Meta:
        # 通過db_table自定義資料表名
        db_table = 'user'

這個表中有我們之前第16節中測試的11條資料,我們來拿這個表來完成相關 SQL 注入的實驗。

圖片描述

我們現在用兩種方式來實現 SQL 注入:

在 Django 中使用原生 SQL 操作 MySQL 資料庫。下面是兩種寫法,分別對應著存在 SQL 注入漏洞和安全的操作:

>>> from django.db import connection
>>> cur = connection.cursor()
# 存在注入漏洞,繞過了判斷語句
>>> cur.execute("select * from user where name='%s' and password='%s'" % ("' or 1=1 #", 'xxx'))
11
# 使用這種方式會避免上述問題
>>> cur.execute("select * from user where name=%s and password=%s", ["' or 1=1#", 'xxx'])
0
>>> 

在 Django 的 ORM 模型中使用 extra() 方法來構建 SQL 注入漏洞:

>>> from hello_app.models import User
# 實現SQL注入
>>> User.objects.all().extra(where=["name='%s' and password='%s'" % ("') or 1=1 limit 1#", 'xx')])
query=b"SELECT `user`.`id`, `user`.`name`, `user`.`password`, `user`.`email` FROM `user` WHERE (name='') or 1=1 limit 1#' and password='xx')  LIMIT 21"
<QuerySet [<User: <test>>]>
# 安全操作
>>> User.objects.all().extra(where=["name=%s and password=%s"], params=["') or 1=1 limit 1#", 'xx'])
query=b"SELECT `user`.`id`, `user`.`name`, `user`.`password`, `user`.`email` FROM `user` WHERE (name='\\') or 1=1 limit 1#' and password='xx')  LIMIT 21"
<QuerySet []>
# 正常取資料操作
>>> User.objects.all().extra(where=["name=%s and password=%s"], params=["test", 'xxxxxx'])
# 這個query是我為了方便在執行sql的地方加了個print語句,列印執行的sql
query=b"SELECT `user`.`id`, `user`.`name`, `user`.`password`, `user`.`email` FROM `user` WHERE (name='test' and password='xxxxxx')  LIMIT 21"
<QuerySet [<User: <test>>]>

注意:為什麼這次注入的語句變成了"') or 1=1 limit 1#",這是因為我發現使用 extra() 方法是生成的 SQL 語句是這樣的 (下面的 query 是我在原始碼新增的一行 print 語句列印的):

>>> User.objects.all().extra(where=["name=%s and password=%s"], params=["test", 'xxxxxx'])
  query=b"SELECT `user`.`id`, `user`.`name`, `user`.`password`, `user`.`email` FROM `user` WHERE (name='test' and password='xxxxxx')  LIMIT 21"
  <QuerySet [<User: <test>>]>

可以看到 extra 將 where 引數放到括號中,為了能注入正確的 SQL語句,就必須要新增 ) 去抵消 # 註釋掉的原右括號,這樣才能正常執行。

到目前位置,我們在 Django 中對 SQL 注入漏洞進行了再現。為了避免 SQL 注入漏洞的方式也比較簡單,主要遵循如下兩個規則即可:

  • 儘量使用 Django 的 ORM 模型提供的方法去操作資料庫
  • 不要使用動態拼接 SQL 的方式,而是將 SQL 語句和引數分開放

4. 小結

本小節中我們主要介紹了 Django 框架在幾種常見的 Web 安全問題上做的一些工作,以及我們在後續開發 Web 專案的過程中要注意的安全規範,避免產生不必要的安全問題。