今回はGitLabの提供するSAST (Static Application Security Testing) 機能を検証します。
背景
GitLabのSAST機能は、アプリケーションコードや一部インフラのマニフェストファイルに対して、GitLab CI/CDパイプライン上でセキュリティスキャンを実行し、脆弱性レポートを報告します。SASTによるスキャンはGitLabのどのプランでも利用可能ですが、Freeプラン以外には付随する様々な機能も利用できます。
SASTは複数のAnalyzerを使って幅広い言語の脆弱性を検知し、Ruleと組み合わせてどんな脆弱性を報告するか制御します。
AnalyzerはSemgrepなどのスキャンツールのラッパーであり、Dockerイメージとして公開されます。デフォルトのイメージはGitLabが管理していますが、利用者がイメージを差し替えることもできます。
またRuleはUltimateプランの利用者はカスタマイズできますが、それ以外はデフォルトのRuleを利用します。
SASTは以下のような言語・フレームワークに対応しています。
- .NET
- C
- C/C++
- Go
- Java
- JavaScript
- Node.js
- PHP
- Python
- React
- Ruby
- Ruby on Rails
- Rust
- TypeScript
- Kubernetes manifest
- Helm chart
SASTを利用するための前提条件はいくつかあります。
- SASTはGitLab CI/CDの test ステージで実行されるため、 test ステージを含める必要があります。
- GitLab Runnerは Docker / Kubernetes (Linux/amd64) のいずれかに対応しています。
- こちらのエラーを避けるため、Dockerは 19.03.0 以外を使用する必要があります。
検証
ここから検証します。まずは特に脆弱性のないコードを対象に、SASTを実行します。ここでは sast-test-01
というGitLab Projectを使用します。
SAST機能はプロジェクト作成時に有効にするか選択できます。
SASTを有効にすると、SASTをJobとして実行する .gitlab-ci.yml
ファイルが作成されます。
このリポジトリに適当にアプリケーションコードの書かれたファイルを配置し、CI/CDパイプラインを実行します。すると semgrep-sast
というJobが実行されます。
Jobを実行した結果はアーティファクトとして保存されます。
今回は特に脆弱性を検知できなかったですが、検知した場合は以下の vulnerabilities
に項目がリストされます。
{ "version": "15.0.7", "vulnerabilities": [], "dependency_files": [], "scan": { "analyzer": { "id": "semgrep", "name": "Semgrep", "url": "https://gitlab.com/gitlab-org/security-products/analyzers/semgrep", "vendor": { "name": "GitLab" }, "version": "4.10.1" }, "scanner": { "id": "semgrep", "name": "Semgrep", "url": "https://github.com/returntocorp/semgrep", "vendor": { "name": "GitLab" }, "version": "1.50.0" }, "type": "sast", "start_time": "2024-01-16T22:16:14", "end_time": "2024-01-16T22:16:21", "status": "success" } }
次は実際に脆弱性を含むコードを対象とします。ここでは WebGoat という脆弱性テストのために提供されるJavaベースのコード群を使用します。
WebGoatはGitHubリポジトリとして提供されているので、まずはこのリポジトリをGitLabにインポートします。
SASTを有効にするため、ここではGitLab画面から有効にします。GitLabでは セキュリティ
→ セキュリティ設定
を選択し、SASTを マージリクエスト経由で設定
というボタンを選択します。
SASTを画面から有効にすると、まず set-sast-config-1
というブランチが作成され、このブランチ宛に .gitlab-ci.yml
が追加され (この時点でパイプラインが実行されます)、Merge requestの作成画面が表示されます。
次に main
ブランチ宛のMerge Requestを作成しますが、Merge requestの画面にはセキュリティスキャンの実行結果をダウンロードするリンクが用意されており、ここからもスキャン結果を確認できます。なおUltimateプランだとMerge request画面上に Security
タブが表示され、スキャン結果も確認できます。
上記MRをマージすることで main
ブランチ上でSASTが有効となります。なおマージ時点でもう一度パイプラインが実行されます。
マージした時点のパイプライン実行結果を見ると、いくつかの脆弱性が報告されているのを確認できます。
{ "version": "15.0.7", "vulnerabilities": [ { "id": "4a20c6e1f2e981794a355ec7d2f93d4e6843a4bff8a988318b6d821c932063b3", "category": "sast", "name": "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')", "description": "SQL Injection is a critical vulnerability that can lead to data or system compromise. By\ndynamically generating SQL query strings, user input may be able to influence the logic of\nthe SQL statement. This could lead to an adversary accessing information they should\nnot have access to, or in some circumstances, being able to execute OS functionality or code.\n\nReplace all dynamically generated SQL queries with parameterized queries. In situations where\ndynamic queries must be created, never use direct user input, but instead use a map or\ndictionary of valid values and resolve them using a user supplied key.\n\nFor example, some database drivers do not allow parameterized queries for `>` or `<` comparison\noperators. In these cases, do not use a user supplied `>` or `<` value, but rather have the\nuser\nsupply a `gt` or `lt` value. The alphabetical values are then used to look up the `>` and `<`\nvalues to be used in the construction of the dynamic query. The same goes for other queries\nwhere\ncolumn or table names are required but cannot be parameterized.\n\nExample using `PreparedStatement` queries:\n```\n// Some userInput\nString userInput = \"someUserInput\";\n// Your connection string\nString url = \"...\";\n// Get a connection from the DB via the DriverManager\nConnection conn = DriverManager.getConnection(url);\n// Create a prepared statement\nPreparedStatement st = conn.prepareStatement(\"SELECT name FROM table where name=?\");\n// Set each parameters value by the index (starting from 1)\nst.setString(1, userInput);\n// Execute query and get the result set\nResultSet rs = st.executeQuery();\n// Iterate over results\nwhile (rs.next()) {\n // Get result for this row at the provided column number (starting from 1)\n String result = rs.getString(1);\n // ...\n}\n// Close the ResultSet\nrs.close();\n// Close the PreparedStatement\nst.close();\n```\n\nFor more information on SQL Injection see OWASP:\nhttps://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html\n", "cve": "semgrep_id:find_sec_bugs.SQL_INJECTION_SPRING_JDBC-1.SQL_INJECTION_JPA-1.SQL_INJECTION_JDO-1.SQL_INJECTION_JDBC-1.SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE-1.SQL_INJECTION-1.SQL_INJECTION_HIBERNATE-1.SQL_INJECTION_VERTX-1.SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING-1:108:108", "severity": "Critical", "scanner": { "id": "semgrep", "name": "Semgrep" }, "location": { "file": "src/main/java/org/owasp/webgoat/lessons/sqlinjection/introduction/SqlInjectionLesson9.java", "start_line": 108 }, "identifiers": [ { "type": "semgrep_id", "name": "find_sec_bugs.SQL_INJECTION_SPRING_JDBC-1.SQL_INJECTION_JPA-1.SQL_INJECTION_JDO-1.SQL_INJECTION_JDBC-1.SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE-1.SQL_INJECTION-1.SQL_INJECTION_HIBERNATE-1.SQL_INJECTION_VERTX-1.SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING-1", "value": "find_sec_bugs.SQL_INJECTION_SPRING_JDBC-1.SQL_INJECTION_JPA-1.SQL_INJECTION_JDO-1.SQL_INJECTION_JDBC-1.SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE-1.SQL_INJECTION-1.SQL_INJECTION_HIBERNATE-1.SQL_INJECTION_VERTX-1.SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING-1" }, { "type": "cwe", "name": "CWE-89", "value": "89", "url": "https://cwe.mitre.org/data/definitions/89.html" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_INJECTION_SPRING_JDBC", "value": "SQL_INJECTION_SPRING_JDBC" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_INJECTION_JPA", "value": "SQL_INJECTION_JPA" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_INJECTION_JDO", "value": "SQL_INJECTION_JDO" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_INJECTION_JDBC", "value": "SQL_INJECTION_JDBC" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE", "value": "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_INJECTION", "value": "SQL_INJECTION" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_INJECTION_HIBERNATE", "value": "SQL_INJECTION_HIBERNATE" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_INJECTION_VERTX", "value": "SQL_INJECTION_VERTX" }, { "type": "find_sec_bugs_type", "name": "Find Security Bugs-SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING", "value": "SQL_PREPARED_STATEMENT_GENERATED_FROM_NONCONSTANT_STRING" } ] }, { (割愛) } ], "dependency_files": [], "scan": { "analyzer": { "id": "semgrep", "name": "Semgrep", "url": "https://gitlab.com/gitlab-org/security-products/analyzers/semgrep", "vendor": { "name": "GitLab" }, "version": "4.10.1" }, "scanner": { "id": "semgrep", "name": "Semgrep", "url": "https://github.com/returntocorp/semgrep", "vendor": { "name": "GitLab" }, "version": "1.50.0" }, "type": "sast", "start_time": "2024-01-16T22:36:29", "end_time": "2024-01-16T22:36:50", "status": "success" } }
その他
Ultimateプランでは、他にも以下のような機能を利用できます。