TECHSTEP

ITインフラ関連の記事を公開してます。

GitLab SAST機能を利用する

今回はGitLabの提供するSAST (Static Application Security Testing) 機能を検証します。

docs.gitlab.com

背景

GitLabのSAST機能は、アプリケーションコードや一部インフラのマニフェストファイルに対して、GitLab CI/CDパイプライン上でセキュリティスキャンを実行し、脆弱性レポートを報告します。SASTによるスキャンはGitLabのどのプランでも利用可能ですが、Freeプラン以外には付随する様々な機能も利用できます。

SASTは複数のAnalyzerを使って幅広い言語の脆弱性を検知し、Ruleと組み合わせてどんな脆弱性を報告するか制御します。

AnalyzerはSemgrepなどのスキャンツールのラッパーであり、Dockerイメージとして公開されます。デフォルトのイメージはGitLabが管理していますが、利用者がイメージを差し替えることもできます。

docs.gitlab.com

またRuleはUltimateプランの利用者はカスタマイズできますが、それ以外はデフォルトのRuleを利用します。

docs.gitlab.com

SASTは以下のような言語・フレームワークに対応しています。

docs.gitlab.com

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プランでは、他にも以下のような機能を利用できます。

  • Merge Request上で検知した脆弱性の確認
  • セキュリティダッシュボード: 脆弱性チェックの結果を評価するためのダッシュボードが利用できます。
  • 偽陽性の検知: 誤検知を認識して精度の向上に役立てられます。
  • 脆弱性の「追跡」: リファクタリングなどでコード自体に変更が入っていない(ファイルの場所が移動しているだけ、など)場合、それを検知します。
  • SAST rulesetのカスタマイズ: SAST機能の挙動を管理するrulesetをカスタマイズし、ルールの上書きや無効化を設定できます。

docs.gitlab.com