freeeの開発情報ポータルサイト

CodeQLでつくる誤検知を減らすためのSAST入門

今年新卒で入社したfreee PSIRTのhikaeです。

freee Developers Advent Calendar 2023の5日目を担当します。

PSIRT(Product Security Incident Response Team)はインシデント発生の予防、早期検知、早期解決、被害の最小化を通して、freeeのプロダクトを堅牢にし、顧客情報を安全に管理するチームです。そのためfreeeの提供する沢山のプロダクトに関わることとなります。

DevSecOpsムーブメントを進めるにあたって、開発サイドでのシフトレフトを積極的に進めています。 本記事ではシフトレフトの手段の一つ、SASTについて入門記事を書いてみました。

PSIRTの日常…DevSecOpsへの取り組み
DevSecOps

SAST(Static Application Security Testing)とは、ソースコードを解析して実装の不備を検知する仕組みです。

先日のGitHub Universeで発表されたCodeScanning Autofixは、 脆弱性の検知から修正まで自動化するアプローチとしてSASTのCodeQLが用いられているようです。

github.blog

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の専用の画面から確認できます。

Code Injectionの例

任意コードが実行できる脆弱性が発見された例

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を使っている場合には検知しなくなります。

https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/#ruby-built-in-support

誤検知を減らすには

ここから言えることは、SASTを効率的に扱うには多少なりともカスタマイズが必要だということです。

CodeQLは独自のパッケージマネージャー、テストツール、言語サーバーが提供されており、静的型付け言語とオブジェクト指向の恩恵を受けながら開発することができます。*7

未対応のフレームワークに対応するためにはどのような手順を踏めば良いのでしょう? 実際の実装手順をActiveRecordを例にみていきます。

まずは何を検出すべきか整理します。

dir = params[:order]] ←Source
dir = "DESC" unless dir == "ASC" ←Sanitizer
User.order("name #{dir}") ←Sink
  1. Source: 外部から入力を受け取ったデータ

  2. Sink: ActiveRecordが提供するSQLが入力可能な引数

  3. 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が検出されることがわかります。

RemoteFlowSourceの検出クエリ

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

Snyk Code Example

SemgrepBearerSonarQubeはTaint Trackingに用途が制限されたカスタムルール機能が備わっています。yamlのスキーマと正規表現がメインで表現力は少ないため誤検知を減らそうとすると労力がかかる反面、学習コストが低いと感じます。

Bearer Custom Rule Example

zenn.dev

docs.bearer.com

docs.sonarsource.com

ちょっとでも気になった方へ

CodeQLの学習資料をまとめました。これを機に静的解析始めてみませんか?

概論

ワークショップ

構文

テンプレート

*1: 言語の正式名称はQL https://link.springer.com/chapter/10.1007/978-3-540-88643-3_3

*2: PrivateプロジェクトではGitHub EnterpriseかつGitHub Advanced Securityのライセンスが必要です

*3: https://railstutorial.jp/

*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/