今年新卒で入社したfreee PSIRTのhikaeです。
freee Developers Advent Calendar 2023の5日目を担当します。
PSIRT(Product Security Incident Response Team)はインシデント発生の予防、早期検知、早期解決、被害の最小化を通して、freeeのプロダクトを堅牢にし、顧客情報を安全に管理するチームです。そのためfreeeの提供する沢山のプロダクトに関わることとなります。
DevSecOpsムーブメントを進めるにあたって、開発サイドでのシフトレフトを積極的に進めています。 本記事ではシフトレフトの手段の一つ、SASTについて入門記事を書いてみました。
SAST(Static Application Security Testing)とは、ソースコードを解析して実装の不備を検知する仕組みです。
先日のGitHub Universeで発表されたCodeScanning Autofixは、 脆弱性の検知から修正まで自動化するアプローチとしてSASTのCodeQLが用いられているようです。
CodeQLは一般的なLinterでは実現できないデータフロー解析ができるとのことで調べてみました。
対象読者
Linterを使ったことがあり、品質やセキュリティに興味あるエンジニアに向けて書いています。 ASTなど一部前提知識が必要になるためご了承ください。
CodeQLとは
Code … QL … ?
直訳するとソースコードに対する問い合わせ言語でしょうか。 GitHub SearchやSourceGraphみたいなものに思えますが、ただの全文検索エンジンではありません。
CodeQLは宣言型のオブジェクト指向クエリ言語とその静的解析エンジンです。*1 検出の部分はSQLに近い構文で、脆弱性を検知するための様々な解析を表現できます。 以下のような脆弱性を検知する能力があります。
https://codeql.github.com/codeql-query-help/full-cwe/
CodeQLの仕組み
GitHub上でCodeQLを有効にすると以下のCIが動作します。*2
steps: - uses: actions/checkout@v3 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: ruby - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2 with: category: "/language:ruby"
Initialize CodeQLでは、静的解析お馴染みのASTの他に、呼び出し関係を表すコールグラフ(CG)、実行経路を表すコントロールフローグラフ(CFG)が作成されます。
ざっとクエリを走らせるとこのような出力を得ることができます。(対象はRails Tutorial)*3
名称 | グラフ |
---|---|
AST | |
Call Graph | |
Control Flow Graph |
検出した脆弱性はGitHubの専用の画面から確認できます。
SASTの問題点
さてここからが本題です。 SASTの大きな課題として、誤検出とコンテキスト不足が挙げられます。*4
誤検出は本当は脆弱性ではないが、脆弱性として検出されることを指します。 *5 これは実際にSASTを導入すると誤検知の多さ(False Positive)に悩まされます。 静的解析は検知漏れをしないことが重要視されるため、安全側に倒した解析を行うことが一因にあります。 これは、開発者にとって効率化の妨げになります。
コンテキスト不足は、フレームワークに関する知識をクエリに対応しなければいけないと言うことです。 これはTaint Trackingの特徴に起因する問題です。 Taint Trackingは、ユーザーの入力値など意図しない値をTaintedとよび、そのデータの発生源であるSource、Taintedな値の無害化を行うSanitizer、データを実行に用いる箇所をSinkとしてそれぞれ定義します。*6 Control Flow Graph中に無害化されないSource-Sink間の経路が発見された場合に脆弱性として検出します。
しかしSource、Sink、Sanitizerはフレームワークごとに異なります。 多くのフレームワークに対応しているものの、 独自に拡張したORMを使っている場合には検知しなくなります。
誤検知を減らすには
ここから言えることは、SASTを効率的に扱うには多少なりともカスタマイズが必要だということです。
CodeQLは独自のパッケージマネージャー、テストツール、言語サーバーが提供されており、静的型付け言語とオブジェクト指向の恩恵を受けながら開発することができます。*7
未対応のフレームワークに対応するためにはどのような手順を踏めば良いのでしょう? 実際の実装手順をActiveRecordを例にみていきます。
まずは何を検出すべきか整理します。
dir = params[:order]] ←Source dir = "DESC" unless dir == "ASC" ←Sanitizer User.order("name #{dir}") ←Sink
Source: 外部から入力を受け取ったデータ
Sink: ActiveRecordが提供するSQLが入力可能な引数
Sanitizer: データが定数マッチでバリデーションされる
1. Source検出
Source検出は、Railsに対応したRemoteFlowSourceを用います。RemoteFlowSourceはDataflow::Nodeを継承したクラスです。
from DataFlow::Node source where source instanceof RemoteFlowSource select source
もしくは
from RemoteFlowSource source select source
でユーザー入力を一覧で取得してみるとparamsやrefererを用いたExpressionが検出されることがわかります。
2. Sink検出
SQLが含まれる箇所を検知する抽象クラスは、SQLクエリビルドするSqlConstructionとSQLを実行するSqlExecutionの二つあり、それぞれフレームワークに合わせたにポリモーフィズムを実装します。
https://github.com/github/codeql/blob/main/ruby/ql/lib/codeql/ruby/Concepts.qll
次にフレームワーク向けの実装ですが、SQLが含まれるメソッドのかどうかは以下のpredicateで検出します。
private predicate sqlFragmentArgumentInner(DataFlow::CallNode call, DataFlow::Node sink) { call = activeRecordQueryBuilderCall([ "delete_all", "delete_by", "exists?", "find_by", ... , "update_all" ]) and sink = call.getArgument(0) or call = activeRecordQueryBuilderCall("calculate") and sink = call.getArgument(1) or ... or call = activeRecordConnectionInstance().getAMethodCall("execute") and sink = call.getArgument(0) }
同様に引数が他からの影響を受ける値かどうかは以下のプログラムで検出します。
private predicate unsafeSqlExpr(Expr sqlFragmentExpr) { // 文字列の補完が入るリテラル sqlFragmentExpr.(StringlikeLiteral).getComponent(_) instanceof StringInterpolationComponent or // 文字列結合 sqlFragmentExpr instanceof AddExpr or // 変数の参照 sqlFragmentExpr instanceof VariableReadAccess or // メソッドの呼び出し sqlFragmentExpr instanceof MethodCall }
上記二つを組み合わせてSource検知が実装されてます。
private predicate sqlFragmentArgument(DataFlow::CallNode call, DataFlow::Node sink) { sqlFragmentArgumentInner(call, sink) and unsafeSqlExpr(sink.asExpr().getExpr()) } class ActiveRecordSqlExecutionRange extends SqlExecution::Range { ActiveRecordSqlExecutionRange() { sqlFragmentArgument(_, this) } override DataFlow::Node getSql() { result = this } }
https://github.com/github/codeql/blob/main/ruby/ql/lib/codeql/ruby/frameworks/ActiveRecord.qll
3. Sanitizer検出
誤検知を防ぐために無害化された箇所を除外することは、とても重要な概念です。 ControlFlowGraph Nodeにおける、様々な条件で無害化するプログラムを検知しています。 ここはフレームワークにかかわらず同じなので端折りますが、ソースコードのコメントなどを読みながら理解してみると様々な工夫が垣間見れます。
https://github.com/github/codeql/blob/main/ruby/ql/lib/codeql/ruby/dataflow/BarrierGuards.qll
4. Taint Tracking
最後に上記で定義した概念を組み合わせてTaint Trackingします。
private module SqlInjectionConfig implements DataFlow::ConfigSig { predicate isSource(DataFlow::Node source) { source instanceof Source } predicate isSink(DataFlow::Node sink) { sink instanceof Sink } predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer } } module SqlInjectionFlow = TaintTracking::Global<SqlInjectionConfig>; from SqlInjectionFlow::PathNode source, SqlInjectionFlow::PathNode sink where SqlInjectionFlow::flowPath(source, sink) select sink.getNode(), source, sink, "このSQLクエリは $@ に依存します。", source.getNode(), "ユーザーが入力可能なデータ"
ここまでが未対応のフレームワークに対応する手順です。 この手順を簡略化するため、Model Editorと呼ばれるGUIでのクエリビルダが試験的に公開されています。
https://codeql.github.com/docs/codeql-for-visual-studio-code/using-the-codeql-model-editor/
その他のツールと比較する
SnykはSnyk Codeと呼ばれる独自言語で同様のことが可能です。Datalogのサブセットであり人間離れしていますが、論理プログラム言語経験者には馴染み深いのかもしれません。
https://docs.snyk.io/scan-using-snyk/snyk-code/custom-rules-beta/the-query-language
SemgrepやBearer、SonarQubeはTaint Trackingに用途が制限されたカスタムルール機能が備わっています。yamlのスキーマと正規表現がメインで表現力は少ないため誤検知を減らそうとすると労力がかかる反面、学習コストが低いと感じます。
ちょっとでも気になった方へ
CodeQLの学習資料をまとめました。これを機に静的解析始めてみませんか?
概論
ワークショップ
構文
テンプレート
- https://github.com/github/vscode-codeql-starter
- https://github.com/GitHubSecurityLab/CodeQL-Community-Packs
- https://github.com/HikaruEgashira/CodeQL-Community-Packs (個人のプロジェクト)
*1: 言語の正式名称はQL https://link.springer.com/chapter/10.1007/978-3-540-88643-3_3
*2: PrivateプロジェクトではGitHub EnterpriseかつGitHub Advanced Securityのライセンスが必要です
*4: https://snyk.io/jp/learn/application-security/static-application-security-testing/
*5: False Positive / 偽陽性
*6: CodeQLでは最近になってSanitizerからBarrierに名称が変更されたが便宜上Sanitizerで統一する、概念について詳しくは概論の記事を参照
*7: https://codeql.github.com/docs/codeql-for-visual-studio-code/