はじめに
Databricks Apps に、デプロイ時にアプリのソースコードを静的解析するコードセキュリティスキャンが入りました。現在はベータです。
Free Edition のワークスペースでもこの機能がオンになっていたので、試してみました。ドキュメントに挙がっている検出カテゴリに当たるコードをわざと書いたアプリをデプロイし、何がどう検出されるかを確認しています。
コードセキュリティスキャンとは
Databricks Apps でアプリをデプロイすると、そのアプリのソースコードが自動でスキャンされます。ドキュメントから要点を拾うと次のとおりです。
- 静的解析で、ソースコードを読むだけです。コードは実行しません
- スキャンの対象はアプリのソースコードだけです。依存ライブラリはスキャンされません
- スキャンの結果でデプロイが止まることはありません
- ソースコードを変えてデプロイしたときにスキャンされます。既存のビルドを使い回すデプロイではスキャンされません
- 結果を見てトリアージするには、アプリに対する
CAN MANAGE権限が必要です
検出カテゴリとして、ドキュメントには次の9つが例として挙がっています。
- ハードコードされた資格情報・シークレット
- SQL インジェクション
- クロスサイトスクリプティング (XSS)
- サーバーサイドリクエストフォージェリ (SSRF)
- パストラバーサル
- 安全でないデシリアライゼーション
- 過度に許容的な CORS 設定
- 安全でない YAML 読み込み
- ユーザー (代理) トークンの露出
検出ルールは Databricks が選定・調整していて、随時追加や更新があるとされています。
脆弱なコードをわざと含めたアプリを用意する
上の9カテゴリに1つずつ当たるコードを入れた Flask アプリを作りました。トークンとパスワードの値はダミーです。行番号をあとで検出結果と照らし合わせるので、デプロイしたものと同じコードをそのまま載せます。
import base64
import os
import pickle
import sqlite3
import requests
import yaml
from flask import Flask, jsonify, request
app = Flask(__name__)
# [1] ハードコードされた資格情報・シークレット (値はダミー)
DATABRICKS_TOKEN = "dapi1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
DB_PASSWORD = "SuperSecret_P@ssw0rd!"
BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files")
def get_db():
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER, name TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'alice'), (2, 'bob')")
return conn
# [7] 過度に許容的な CORS 設定
@app.after_request
def add_cors_headers(resp):
resp.headers["Access-Control-Allow-Origin"] = "*"
resp.headers["Access-Control-Allow-Credentials"] = "true"
return resp
@app.route("/")
def index():
return "<h1>security scan test app</h1>"
# [2] SQL インジェクション
@app.route("/user")
def user():
name = request.args.get("name", "")
rows = get_db().execute(f"SELECT id, name FROM users WHERE name = '{name}'").fetchall()
return jsonify(rows)
# [3] クロスサイトスクリプティング (XSS)
@app.route("/hello")
def hello():
name = request.args.get("name", "")
return f"<h1>Hello {name}</h1>"
# [4] サーバーサイドリクエストフォージェリ (SSRF)
@app.route("/fetch")
def fetch():
url = request.args.get("url", "")
return requests.get(url, timeout=5).text
# [5] パストラバーサル
@app.route("/file")
def read_file():
name = request.args.get("name", "")
with open(os.path.join(BASE_DIR, name)) as f:
return f.read()
# [6] 安全でないデシリアライゼーション
@app.route("/load", methods=["POST"])
def load():
obj = pickle.loads(base64.b64decode(request.get_data()))
return jsonify({"type": str(type(obj))})
# [8] 安全でない YAML 読み込み
@app.route("/config", methods=["POST"])
def config():
data = yaml.load(request.get_data(as_text=True), Loader=yaml.Loader)
return jsonify({"keys": list(data.keys()) if isinstance(data, dict) else []})
# [9] ユーザー (代理) トークンの露出
@app.route("/whoami")
def whoami():
token = request.headers.get("x-forwarded-access-token")
print(f"user token: {token}")
return jsonify({"token": token})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("DATABRICKS_APP_PORT", "8000")))
app.yaml と requirements.txt は次のとおりです。
command: ["python", "app.py"]
flask
requests
pyyaml
脆弱なエンドポイントを持つアプリなので、試すときは確認が終わったら停止か削除をしてください。
ノートブックからデプロイする
デプロイはノートブックから Databricks SDK で行いました。上の3ファイルをワークスペースのフォルダ /Workspace/Users/<ユーザー名>/security-scan-test に書き出してある前提です。
%pip install -U databricks-sdk
dbutils.library.restartPython()
from databricks.sdk import WorkspaceClient
from databricks.sdk.service.apps import App, AppDeployment
w = WorkspaceClient()
me = w.current_user.me().user_name
APP_NAME = "security-scan-test"
SOURCE_DIR = f"/Workspace/Users/{me}/{APP_NAME}"
# アプリを作成します。コンピュートが起動するまで数分かかります
app = w.apps.create_and_wait(app=App(name=APP_NAME, description="code security scanning test"))
# ワークスペースのフォルダにあるソースコードをデプロイします
d = w.apps.deploy_and_wait(
app_name=APP_NAME,
app_deployment=AppDeployment(source_code_path=SOURCE_DIR),
)
print(d.status.state, d.status.message)
AppDeploymentState.SUCCEEDED App started successfully
脆弱なコードを含んでいても、デプロイは普通に成功してアプリも起動します。スキャン結果でデプロイが止まらないというドキュメントの説明どおりです。
スキャン結果を見る
ワークスペース全体のセキュリティページ
アプリの画面の左メニューに「アプリ」と並んで「セキュリティ」があります。ここを開くと、ワークスペース内のすべてのアプリのスキャン結果がまとめて表示されます。
今回はアプリが1つだけなので、リスクのあるアプリが1件、未解決の検出結果が7件です。重要度の内訳は「重大」が3件、「警告」が4件でした。「上位のチェック」には、検出したルールの名前が並びます。アプリが増えてくると、どのルールがどれだけのアプリで当たっているかをここで見渡せます。
アプリ単位のセキュリティページ
一覧の security-scan-test を開くと、アプリ単位の結果が表示されます。アプリの画面の左メニューにも「概要」「デプロイメント」「ログ」などと並んで「セキュリティ」があり、そこからも同じページを開けます。検出結果はファイルごとにまとまっていて、各行にルール名、ファイルと行番号、重要度が出ます。
上部の「コードバージョン」で、どのデプロイのスキャン結果かを切り替えられます。デプロイを重ねたあとで、過去のコードの結果を見返すときに使います。
検出結果を展開すると、問題の説明と修正の方向が英語で表示されます。ここから「解決済みとしてマーク」と「無視する」を操作できます。
たとえば YAML の検出では、yaml.load に安全でない Loader を渡していることを指摘したうえで、yaml.safe_load を使うよう書かれています。SQL インジェクションの検出では、パラメータ化したクエリを使うよう書かれています。
9カテゴリのうち何が検出されたか
検出された7件を、app.py の行番号と照らし合わせました。
| 行 | ルール | 重要度 | 該当するコード |
|---|---|---|---|
| 13 | tmp.databricks.auth.hardcoded-secret-assignment |
警告 | DATABRICKS_TOKEN = "dapi..." |
| 14 | tmp.databricks.auth.hardcoded-secret-assignment |
警告 | DB_PASSWORD = "..." |
| 30 | tmp.databricks.auth.hardcoded-secret-assignment |
警告 | resp.headers["Access-Control-Allow-Credentials"] = "true" |
| 43 | tmp.databricks.sql-injection.fstring-where |
重大 | f-string で組み立てた SQL |
| 43 | tmp.databricks.sql-injection.fstring-select |
重大 | 同上 |
| 58 | tmp.databricks.ssrf.requests-user-url |
警告 | requests.get(url) |
| 79 | tmp.databricks.deserialization.yaml-unsafe-load |
重大 | yaml.load(..., Loader=yaml.Loader) |
カテゴリ単位で見ると、今回のコードで検出されたのは次の4つです。
- ハードコードされた資格情報・シークレット
- SQL インジェクション
- SSRF
- 安全でない YAML 読み込み
残りの5つは、今回のコードでは検出されませんでした。
- XSS (
/helloで入力をそのまま HTML として返す) - パストラバーサル (
/fileで入力されたパスをopenする) - 安全でないデシリアライゼーションのうち
pickle.loads - 過度に許容的な CORS 設定
- ユーザートークンの露出 (
x-forwarded-access-tokenをレスポンスとログに出す)
ドキュメントのカテゴリはあくまで例で、ルールは Databricks が選定して随時更新していくものです。今回書いたパターンが、ベータの現時点のルールにはまだ含まれていないのだと思います。同じカテゴリでも、書き方によっては検出される可能性があります。検出されなかったことは、そのコードが安全だという意味ではありません。この点はドキュメントにも明記されています。
CORS の設定行がシークレットとして検出された
30行目の検出は、シークレットの検出ではありません。この行は CORS のヘッダーを設定しているだけで、資格情報は含まれていません。
resp.headers["Access-Control-Allow-Credentials"] = "true"
キーに Credentials という語が入っていて、そこに文字列を代入しているので、シークレットの代入として拾われたのだと思われます。名前と値の形から判定するルールなので、当然といえば当然です。
こういう誤検知は「無視する」で片付けます。無視した検出は一覧に「無視」の印が付いて残り、「再度開く」で元に戻せます。トリアージの状態はデプロイをまたいで保持されるので、次のデプロイで同じ行が再び検出されても、無視した状態のまま残ります。誤検知をそのつど確認し直さなくて済むのは、運用するうえで助かる点です。
まとめ
Databricks Apps のコードセキュリティスキャンを Free Edition で試して分かったことをまとめます。
- Free Edition のワークスペースでも機能がオンになっていて、そのまま使えた
- 脆弱なコードを含むアプリでもデプロイは成功する。スキャンはデプロイを止めず、結果を見せるだけ
- 結果は、アプリ画面の左メニューの「セキュリティ」から、ワークスペース全体とアプリ単位の両方で見られる
- 9カテゴリのコードを入れたアプリで、検出されたのはシークレット、SQL インジェクション、SSRF、YAML 読み込みの4カテゴリ、計7件だった
- XSS、パストラバーサル、
pickle.loads、CORS、ユーザートークンの露出は、今回のコードでは検出されなかった - CORS ヘッダーの
Access-Control-Allow-Credentialsを設定する行が、シークレットの代入として検出された。こうした誤検知は「無視する」で片付けられ、その状態はデプロイをまたいで残る
一番の収穫は、検出されたものと検出されなかったものを、行番号の単位で確認できたことでした。「検出なし」はアプリが安全だという保証ではありません。スキャンは、コードレビューで見落としやすい典型的なパターンを拾う仕組みと考えるのがよさそうです。デプロイのたびに自動で走って何も設定がいらないので、Databricks Apps を使っているなら、一度セキュリティページを開いてみることをお勧めします。



