pythonのstrip関数で対象としているSpaceとは

実例

「2年目」の行動計画の1つリサーチャーのブログから気になったところを調べて勉強したいと思います。

元々読んでいた記事

「Cookie Chaos: How to bypass __Host and __Secure cookie prefixes」by Zakhar Fedotkin

Cookie Chaos: How to bypass __Host and __Secure cookie prefixes
Browsers added cookie prefixes to protect your sessions and stop attackers from setting harmful cookies. In this post, y...

記事の要約

背景

ブラウザは、セッションを保護し攻撃者による悪意のあるCookie設定を防ぐために、__Host-__Secure-のCookieプレフィックス機能を導入しました。これらは以下の制限を課すものです:

  • __Host-:ドメインやパス属性の制限、セキュア接続の必須化
  • __Secure-:セキュア接続での送信のみ許可

攻撃手法の核心

記事では、ブラウザとサーバー間でのCookie処理の不整合を悪用した2つの主要な攻撃手法を紹介しています。

1. UTF-8エンコーディングを使った手法

攻撃者は、Unicodeの空白文字(例:U+2000)を使って制限付きCookieを偽装できます:

javascript
document.cookie = `${String.fromCodePoint(0x2000)}__Host-name=injected; Domain=.example.com; Path=/;`

ブラウザはこれを制限のない通常のCookieとして扱いますが、サーバー側(DjangoやASP.NETなど)では空白文字を削除して正規化するため、結果的に__Host-nameとして解釈されます。

2. レガシーCookie解析の悪用

Java系Webサーバー(Apache Tomcat、Jettyなど)では、$Version=1で始まるCookieヘッダーでレガシー解析モードに切り替わり、単一のCookie文字列が複数の別々のCookieとして解釈されます:

javascript

document.cookie = `$Version=1,__Host-name=injected; Path=/somethingreallylong/; Domain=.example.com;`

実際の攻撃シナリオ

XSS脆弱性がある場面で、__Host-プレフィックス付きCookieが適切にエスケープされずにWebページに反映される場合:

  1. 攻撃者が偽装したCookieを注入
  2. ブラウザは元のCookieと攻撃者のCookieの両方を送信
  3. サーバー側で同名Cookie競合時に後者(攻撃者のもの)が優先される
  4. 結果として XSS、セッション固定化、権限昇格などが可能

対応と課題

記事ではサーバ側の動作は技術的に正しいことを示しております。

According to the original RFC 6265, the Cookie header is defined as a sequence of octets, not characters. This means the browser sends raw bytes on the wire, and it’s the server’s responsibility to decode those bytes into a string.

Djangoチームは、この問題について「信頼できないサブドメインからのCookieを許可することの危険性は既に警告済み」として、セキュリティ脆弱性とは認めない姿勢を示しています。

Django responded to my vulnerability report:

The official Django documentation has a warning against permitting cookies from untrusted subdomains as this is vulnerable to attacks: https://docs.djangoproject.com/en/5.0/topics/http/sessions/#topics-session-security. As this attack relies on this, this will not be treated as a security vulnerability.

まとめ

この研究は、最強のブラウザ側保護機能を使用していても、ブラウザとバックエンド間でのCookie解釈の不一致により、Cookieの機密性と完全性の保証が静かに破られる可能性があることを示しています。記事ではBurp Suite用の検証ツールも提供されています。

Djangoの挙動調査

Django uses Python’s built-in .strip() method to process cookie keys and values. This method removes a wide range of Unicode whitespace characters, including [133, 160, 5760, 8192–8202, 8232, 8233, 8239, 8287, 12288], effectively treating them as a space.

記事ではDjangoはstrip()を使いその中でその中でマルチバイトの空白を削除しているとのことです。
今回は勉強のためにpythonのstrip()のソースを見たいと思います。

python公式リポジトリ

メインリポジトリ: https://github.com/python/cpython

バージョン:3.15.0

str.strip()の定義

str.strip() → unicode_strip() → unicode_strip_impl()と呼び出しています。

cpython / Include / unicodeobject.h

#define UNICODE_STRIP_METHODDEF    \
    {"strip", _PyCFunction_CAST(unicode_strip), METH_FASTCALL, unicode_strip__doc__},
tatic PyObject *
unicode_strip_impl(PyObject *self, PyObject *chars);

static PyObject *
unicode_strip(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
{
    PyObject *return_value = NULL;
    PyObject *chars = Py_None;

    if (!_PyArg_CheckPositional("strip", nargs, 0, 1)) {
        goto exit;
    }
    if (nargs < 1) {
        goto skip_optional;
    }
    chars = args[0];
skip_optional:
    return_value = unicode_strip_impl(self, chars);

exit:
    return return_value;
}

unicode_strip_impl()の実装

unicode_strip_impl() → do_argstrip() → do_strip()と呼び出しています。

cpython / Objects /unicodeobject.c

static PyObject *
unicode_strip_impl(PyObject *self, PyObject *chars)
/*[clinic end generated code: output=ca19018454345d57 input=8bc6353450345fbd]*/
{
    return do_argstrip(self, BOTHSTRIP, chars);
}
static PyObject *
do_argstrip(PyObject *self, int striptype, PyObject *sep)
{
    if (sep != Py_None) {
        if (PyUnicode_Check(sep))
            return _PyUnicode_XStrip(self, striptype, sep);
        else {
            PyErr_Format(PyExc_TypeError,
                         "%s arg must be None or str",
                         STRIPNAME(striptype));
            return NULL;
        }
    }

    return do_strip(self, striptype);
}

do_argstrip()ではsepという引数の有無によって処理が分岐しています。

sepにはstrip()を呼び出したときに指定するstrip対象の文字となりますが、指定しないことも可能です。
今回のケースについては指定していない時に除去するスペースの対象を見たいのでdo_strip()を見ます。

static PyObject *
do_strip(PyObject *self, int striptype)
{
    Py_ssize_t len, i, j;

    len = PyUnicode_GET_LENGTH(self);

    if (PyUnicode_IS_ASCII(self)) {
        const Py_UCS1 *data = PyUnicode_1BYTE_DATA(self);

        i = 0;
        if (striptype != RIGHTSTRIP) {
            while (i < len) {
                Py_UCS1 ch = data[i];
                if (!_Py_ascii_whitespace[ch])
                    break;
                i++;
            }
        }

        j = len;
        if (striptype != LEFTSTRIP) {
            j--;
            while (j >= i) {
                Py_UCS1 ch = data[j];
                if (!_Py_ascii_whitespace[ch])
                    break;
                j--;
            }
            j++;
        }
    }
    else {
        int kind = PyUnicode_KIND(self);
        const void *data = PyUnicode_DATA(self);

        i = 0;
        if (striptype != RIGHTSTRIP) {
            while (i < len) {
                Py_UCS4 ch = PyUnicode_READ(kind, data, i);
                if (!Py_UNICODE_ISSPACE(ch))
                    break;
                i++;
            }
        }

        j = len;
        if (striptype != LEFTSTRIP) {
            j--;
            while (j >= i) {
                Py_UCS4 ch = PyUnicode_READ(kind, data, j);
                if (!Py_UNICODE_ISSPACE(ch))
                    break;
                j--;
            }
            j++;
        }
    }

    return PyUnicode_Substring(self, i, j);
}

この関数ではまずASCII範囲の文字列か確認して処理を分岐しています。
そしてU+2000(非ASCII)の処理を見ると、先頭部分から文字を読み込みPy_UNICODE_ISSPACE()でスペースか判定しているところを見つけました。

Py_UNICODE_ISSPACE()の実装

Py_UNICODE_ISSPACE() → _PyUnicode_IsWhitespace() と呼び出しています。

cpython / Include / unicodeobject.h

static inline int Py_UNICODE_ISSPACE(Py_UCS4 ch) {
    if (ch < 128) {
        return _Py_ascii_whitespace[ch];
    }
    return _PyUnicode_IsWhitespace(ch);
}

cpython / Objects /unicodetype_db.h

/* Returns 1 for Unicode characters having the bidirectional
 * type 'WS', 'B' or 'S' or the category 'Zs', 0 otherwise.
 */
int _PyUnicode_IsWhitespace(const Py_UCS4 ch)
{
    switch (ch) {
    case 0x0009:
    case 0x000A:
    case 0x000B:
    case 0x000C:
    case 0x000D:
    case 0x001C:
    case 0x001D:
    case 0x001E:
    case 0x001F:
    case 0x0020:
    case 0x0085:
    case 0x00A0:
    case 0x1680:
    case 0x2000:
    case 0x2001:
    case 0x2002:
    case 0x2003:
    case 0x2004:
    case 0x2005:
    case 0x2006:
    case 0x2007:
    case 0x2008:
    case 0x2009:
    case 0x200A:
    case 0x2028:
    case 0x2029:
    case 0x202F:
    case 0x205F:
    case 0x3000:
        return 1;
    }
    return 0;
}

各値の詳細

ASCII制御文字・空白文字

  • 0x0009: \t (TAB) – タブ文字
  • 0x000A: \n (LINE FEED) – 改行文字
  • 0x000B: \v (VERTICAL TAB) – 垂直タブ
  • 0x000C: \f (FORM FEED) – 改ページ文字
  • 0x000D: \r (CARRIAGE RETURN) – 復帰文字
  • 0x001C: FILE SEPARATOR – ファイル分離子
  • 0x001D: GROUP SEPARATOR – グループ分離子
  • 0x001E: RECORD SEPARATOR – レコード分離子
  • 0x001F: UNIT SEPARATOR – ユニット分離子
  • 0x0020: SPACE – 通常のスペース文字

Latin-1補助文字

  • 0x0085: NEXT LINE (NEL) – 次行文字
  • 0x00A0: NO-BREAK SPACE – 改行されないスペース

その他のUnicode空白文字

  • 0x1680: OGHAM SPACE MARK – オガム文字のスペース
  • 0x2000: EN QUAD – enクワッド(enの幅の空白)
  • 0x2001: EM QUAD – emクワッド(emの幅の空白)
  • 0x2002: EN SPACE – enスペース
  • 0x2003: EM SPACE – emスペース
  • 0x2004: THREE-PER-EM SPACE – 1/3 emスペース
  • 0x2005: FOUR-PER-EM SPACE – 1/4 emスペース
  • 0x2006: SIX-PER-EM SPACE – 1/6 emスペース
  • 0x2007: FIGURE SPACE – 数字幅スペース
  • 0x2008: PUNCTUATION SPACE – 句読点幅スペース
  • 0x2009: THIN SPACE – 細いスペース
  • 0x200A: HAIR SPACE – 極細スペース

行分離文字

  • 0x2028: LINE SEPARATOR – 行分離子
  • 0x2029: PARAGRAPH SEPARATOR – 段落分離子

その他の特殊空白

  • 0x202F: NARROW NO-BREAK SPACE – 狭い改行されないスペース
  • 0x205F: MEDIUM MATHEMATICAL SPACE – 中程度の数式スペース
  • 0x3000: IDEOGRAPHIC SPACE – 全角スペース(日本語など)

タイトルとURLをコピーしました