SqliteAccountInfo profile name validation uses "re.match", so only the first character is checked

未關閉 適合新手
#629 0 則留言 0 個 reaction 已指派 0 人 在 GitHub 檢視

維護者通常 1 天內回覆

@nazarli-shabnam 已經在處理了。

開始於 2026年11月5日。

  • #643 來自 @nazarli-shabnam —— 未關閉

評估

難度
2/5
預估耗時
1-3 小時
新手友好度
75/100
Issue 類型
缺陷
描述清晰度
描述清楚
活躍度
活躍
技術堆疊
python, sqlite
領域
backend, security

研究方向

閱讀 b2sdk/_internal/account_info/sqlite_account_info.py,尤其是 get_user_account_info_path 和 B2_ACCOUNT_INFO_PROFILE_NAME_REGEXP,然後檢查現有的 test_invalid_profile_name 測試。更新驗證邏輯,使其拒絕設定檔名稱中任何位置的無效字元,並在測試中加入一個有效的開頭字元後接惡意後綴的情況;當這些名稱引發 ValueError,且有效設定檔仍可正常運作時,工作就完成了。

由索引模型根據 Issue 內容生成。

描述

Description

get_user_account_info_path validates profile with B2_ACCOUNT_INFO_PROFILE_NAME_REGEXP.match(profile) (b2sdk/_internal/account_info/sqlite_account_info.py:112), and the pattern at :30 is unanchored, so re.match accepts any string whose first character is in [a-zA-Z0-9_\-] and never looks at the rest. The value is then interpolated into the database path at :115, :136 and :142 and handed to os.open and sqlite3.connect without normalisation, so a name like a/../../../../../tmp/planted reaches the filesystem with its .. components intact and the credential database, which holds a live application key, is created outside the directory the SDK selected. Over a corpus of 108 names built from valid leading characters and 21 hostile suffixes, 103 were accepted and 50 of those resolved outside {XDG_CONFIG_HOME}/b2. Two things keep this low rather than worse: nothing inside the SDK passes untrusted input as profile, and on POSIX the first injected path component has to already exist as a directory, which the SDK never creates, so an attacker who can create it already has write access to the config directory. We are filing it in the open for that reason. It still seems worth fixing, because the pattern exists specifically to constrain this value and currently does not, and the one test covering it, test_invalid_profile_name, uses '&@(*$', whose first character is already outside the class, so it passes against the unanchored pattern.

Reproduction

On master @ ffa02b32 with Python 3.10.19, in a container built from a fresh clone at that ref, with the ~/.config/b2/db-a directory created to satisfy the POSIX precondition. Build with this Dockerfile, save the script below as poc.py, then run docker build -t b2poc . && docker run --rm -u 1001 -v "$PWD/poc.py:/poc.py:ro" b2poc python /poc.py:

FROM python:3.10.19-slim-bookworm
RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends git >/dev/null
RUN git clone -q https://github.com/Backblaze/b2-sdk-python.git /src \
 && git -C /src checkout -q ffa02b3264bc0e7069ef2c8b25facd9a467e65a6 \
 && pip install --no-cache-dir -q /src
RUN useradd -m -u 1001 victim
import os, sqlite3
from b2sdk.v3 import SqliteAccountInfo

PROFILE = 'a/../../../../../tmp/planted'   # accepted by .match(), rejected by .fullmatch()
print('resolved :', SqliteAccountInfo.get_user_account_info_path(profile=PROFILE))
os.makedirs(os.path.expanduser('~/.config/b2/db-a'))   # POSIX needs the first component
info = SqliteAccountInfo(profile=PROFILE)
info.set_auth_data(
    account_id='acct', auth_token='tok', api_url='https://api', download_url='https://dl',
    absolute_minimum_part_size=5000000, recommended_part_size=100, realm='production',
    application_key='VICTIM-SECRET-KEY', s3_api_url='https://s3', application_key_id='keyid',
    allowed=SqliteAccountInfo.DEFAULT_ALLOWED)
print('created  :', os.path.realpath(info.filename))
print('key there:', sqlite3.connect('/tmp/planted.sqlite')
      .execute('select application_key from account').fetchone()[0])
resolved : /home/victim/.config/b2/db-a/../../../../../tmp/planted.sqlite
created  : /tmp/planted.sqlite
key there: VICTIM-SECRET-KEY
Suggested remediation

Validate the whole string:

--- a/b2sdk/_internal/account_info/sqlite_account_info.py
+++ b/b2sdk/_internal/account_info/sqlite_account_info.py
@@ -109,7 +109,7 @@ class SqliteAccountInfo(UrlPoolAccountInfo):
 
     @classmethod
     def get_user_account_info_path(cls, file_name: str | None = None, profile: str | None = None):
-        if profile and not B2_ACCOUNT_INFO_PROFILE_NAME_REGEXP.match(profile):
+        if profile and not B2_ACCOUNT_INFO_PROFILE_NAME_REGEXP.fullmatch(profile):
             raise ValueError(f'Invalid profile name: {profile}')

One adjacent thing I noticed while reproducing this, which is separate and not addressed by the above: _validate_database (:144) only calls _create_database when the file does not already exist, and _create_database is the only place mode 0600 is set, so a database file that is already present keeps its permissions. Starting from a mode 0666 file, the mode after credentials were stored was still 0o666 and the application key was readable from it.

Credits
  • Found by OSTIF using AI with their own harness.
  • Manually verified, fixed, and reported by Shielder.
主要語言
Python
星號
214
分支
68
平均合併
51 分鐘
30 天內合併 PR
6

環境準備

  • 沒有 Dockerfile 或 Docker Compose 檔案
  • 沒有 Pull Request 範本
  • 閱讀貢獻指南

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

Backblaze/b2-sdk-python 的其他 Issue

查看 Backblaze/b2-sdk-python 的全部 Issue

相似的 Issue

更多 Python Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。