In this article
GolangのSQLインジェクションを例で解説
Golangでバックエンド開発を始めたばかりなら、安全なコーディングの慣習を把握しておく必要があります。アプリケーションセキュリティの観点では、GoにおけるSQLインジェクションの意味と、SQLインジェクションによるコードの脆弱性を修正する方法を理解することがその一つです。
この例では、Golang用のGin WebアプリケーションフレームワークとSQLiteライブラリを使い、HTTPサーバーのルートを公開してGolangのSQLインターフェースとやり取りします。この実験を通じて、SQLiteデータベースやその他のデータベースでSQLインジェクションを実証する攻撃を実行する方法を学びます。
GolangのSQLプログラム
GolangのSQLインジェクションの例に入る前に、適切な開発環境を整えることが重要です。必要なGoモジュールをインストールし、依存関係を効率的に管理できるようプロジェクトを設定します。
SQLインターフェースには、github.com/mattn/go-sqlite3パッケージとgithub.com/jmoiron/sqlxパッケージを使用します。
Goモジュールで依存関係を管理する
Goプロジェクトで依存関係を管理する標準的な方法として、Goモジュールが広く使われるようになりました。プロジェクトが依存するパッケージのバージョンを指定できるため、さまざまな環境で一貫性を保てます。Goモジュールを使うには、go.modファイルでプロジェクトを初期化します。
SnykはSnyk Code(SASTツール)と、サードパーティの依存関係を対象とするSCAツールのSnyk Open Sourceを通じて、Golangを強力にサポートしています。Goコードを常にスキャンして脆弱性を検出しましょう。Snykなら開発ワークフローにスムーズに組み込めます。
GolangのSQLの例を始めるには、プロジェクトディレクトリに移動して、次のコマンドを実行します。
go mod init your_project_nameこのコマンドは、依存関係とそのバージョンを記録するgo.modファイルをプロジェクトディレクトリに作成します。
GolangのSQLインターフェースパッケージをインストールする
github.com/mattn/go-sqlite3パッケージは、SQLiteデータベースのインターフェースを提供するGoライブラリです。GolangのSQLプログラムからSQLiteデータベースに対してSQLクエリを実行するために必要で、このGoアプリの例でも使用します。このパッケージをインストールするには、次のコマンドを実行します。
go get github.com/mattn/go-sqlite3
このコマンドはパッケージをダウンロードし、go.modファイルに追加します。これでプロジェクト内でパッケージを使い、SQLiteデータベースとやり取りできるようになります。
同様に、Go標準のdatabase/sqlパッケージを拡張するgithub.com/jmoiron/sqlxパッケージもインストールします。名前付きクエリのサポートや構造体へのスキャンなどの機能が追加され、GolangのSQLアプリケーションでのデータベース操作を簡素化できます。インストールするには、次を実行します。
go get github.com/jmoiron/sqlx次に、以下のようにプログラムでこれらのGoパッケージをインポートします。
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"time"
"log/slog"
// Gin web framework
"github.com/gin-gonic/gin"
// SQLite database driver and query builder
_ "github.com/mattn/go-sqlite3"
"github.com/jmoiron/sqlx"
)上記のGoモジュールのインポート手順からわかるように、このGolangアプリケーションはHTTP APIとして、GETルートとPOSTルートを公開します。これを基に、GolangのSQLプログラムのデモを構築します。
GolangのSQLインターフェース
このGolangのWebアプリケーションは、HTTPルートを通じてSQLデータベースとやり取りします。/cloudpawnery/imageのHTTPルートはGETリクエストを受け付け、クエリパラメーターtenantIDを取得します。この値を使って、SQLiteデータベース内で画像変換が行われたすべてのファイルレコードを検索します。
GolangのSQLインターフェースは、ファイルのJSONレスポンスを返す次のGET HTTPルートAPIで示されています。
router.GET("/cloudpawnery/image", func(c *gin.Context) {
// get tenantID from query parameter
tenantID := c.Query("tenantID")
// pseudo-code :
for loop-over-db-rows
if err != nil {
// handle errors
}
// populate an array of files:
files = append(files, f)
}
// return files as an object in the JSON response
c.JSON(http.StatusOK, gin.H{"files": files})
})ここからは、プログラムのGolang SQLインターフェース部分に注目しましょう。
Goモジュールのsqlxを使ってSQLiteデータベースを開き、Queryx関数でSELECTクエリをデータベースに送信します。このクエリではWHEREフィルターを使い、HTTP GETリクエストのクエリパラメーターからtenantIDを取得します。
db, err := sqlx.Open("sqlite3", "./mydb.db")
defer db.Close()
if err != nil {
slog.Error("Failed to open database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open database"})
return
}
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")
if err != nil {
slog.Error("Failed to query database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query database"})
return
}
defer rows.Close()rows変数を通じてデータベースレコードへの参照を取得したら、ループで処理し、SQLクエリの結果を空のFile型配列filesに割り当てます。
var files []File
for rows.Next() {
var f File
err := rows.StructScan(&f)
if err != nil {
slog.Error("Failed to scan database row", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan database row"})
return
}
files = append(files, f)
}SQLクエリの結果が返される限り、ループに入ります(そのためrows.Next() を呼び出します)。次にvar f FileでFileエントリーを1つ用意し、sqlx Goモジュールのrows.StructScan()を使ってクエリ結果から情報を抽出し、File型変数fに割り当てます。
以下はHTTP GETルートAPIの全体です。
router.GET("/cloudpawnery/image", func(c *gin.Context) {
// get tenantID from query parameter
tenantID := c.Query("tenantID")
db, err := sqlx.Open("sqlite3", "./mydb.db")
defer db.Close()
if err != nil {
slog.Error("Failed to open database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open database"})
return
}
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")
if err != nil {
slog.Error("Failed to query database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query database"})
return
}
defer rows.Close()
var files []File
for rows.Next() {
var f File
err := rows.StructScan(&f)
if err != nil {
slog.Error("Failed to scan database row", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan database row"})
return
}
files = append(files, f)
}
c.JSON(http.StatusOK, gin.H{"files": files})
})しかし、アプリケーションセキュリティはどうでしょうか。上記のコードで何が問題になる可能性があるでしょうか。SQLインジェクションです。詳しく見ていきましょう。
SQLiteにおけるSQLインジェクション
SQLインジェクションは、攻撃者がGolangアプリケーションからデータベースに送られるSQLクエリに介入できる、重大なセキュリティ脆弱性です。2024年になってもSQLインジェクションは依然として大きな懸念事項であり、サイバーセキュリティの攻撃手法やリスクのリストに頻繁に取り上げられています。入力データを操作することで、攻撃者は任意のSQLコードを実行し、機密データへの不正アクセス、データベース内容の変更、さらには管理操作の実行を行う可能性があります。SQLインジェクションの影響は深刻で、データ侵害やデータの損失、組織の評判への重大な損害につながるおそれがあります。
GolangアプリケーションのSQLインジェクション脆弱性を実証する
GolangアプリケーションにおけるSQLインジェクション脆弱性を説明するため、上記のアプリケーションを基に、GolangのSQLインターフェースとしてsqlxパッケージを使った例を作成します。
HTTP APIインターフェースは、JSONを返すGETリクエストです。クエリパラメーターtenantIDを介してテナント識別子を受け取ります。利用者(または攻撃者)は、次のようなリクエストを送ることができます。
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712
Content-Type: application/jsontenantIDが正しく(かつ、このデータへのアクセスが許可されて)いれば、次のようなレスポンスが返ります。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 01 Dec 2024 12:37:05 GMT
Content-Length: 248
Connection: close
{
"files": [
{
"ID": 1,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "3971533981712",
"CreatedAt": "0001-01-01T00:00:00Z"
}
]
}問題なさそうです。
しかし、Golangプログラムがこのデータを返すために実行しているSQLクエリをリバースエンジニアリングできる可能性があります。たとえば、filesテーブルをクエリし、tenantIDやtenant_ID、tenant_identifierなどのテナント識別子フィールドでファイルの結果を絞り込んでいると推測できます。
実際、GETルートのパラメーターに対して作成したクエリコードを調べると、まさにそのようになっていることがわかります。
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")これが、Golangプログラムに作り込んでしまった脆弱なSQLインジェクションコードです。
問題の根本原因は、ユーザー入力(クエリパラメーター)から取得したtenantID変数をSQLクエリ自体に追加するために、文字列連結を行うコードにあります。この安全でないSQLコーディングの慣習によって、SQLiteでSQLインジェクションの脆弱性が生じます。
このことから、tenantIDクエリパラメーターを操作して、Golangプログラム内の元のSQLクエリの意味を変えるのがいかに簡単かがわかります。次のHTTPリクエストを送信できます。
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712' OR tenant_id='432423
Content-Type: application/jsontenantIDの値を変更し、シングルクォートでWHERE句のフィールド代入を「閉じ」、新しい論理式OR tenant_id=’<some-number>を作成します。これにより、データベース内の別のレコードに絞り込めます。また、閉じるシングルクォートが必要ない巧妙な方法です。なぜ不要なのでしょうか。それは、SQLクエリのコード自体がシングルクォートを付けるためです。
この仕組みを利用すると、上記のHTTPリクエストを送信することで、両方のテナントのレコードが返されます。
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 01 Dec 2024 12:37:05 GMT
Content-Length: 248
Connection: close
{
"files": [
{
"ID": 1,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "3971533981712",
"CreatedAt": "0001-01-01T00:00:00Z"
},
{
"ID": 2,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "432423",
"CreatedAt": "0001-01-01T00:00:00Z"
}
]
}認証を狙ったGolangのSQLインジェクション
SQLインジェクションの意味を開発者に示す、より典型的なGolangのSQLインジェクションの例として、次のGolangコードを見てみましょう。
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./example.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
username := "admin' OR '1'='1"
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
rows, err := db.Query(query)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("User: %d, %s\n", id, name)
}
}上記のGolangコードスニペットは、OR 1=1として知られる、SQLインジェクションによるよくある認証バイパスを基にしています。もちろん、このGolangコードには他にも多くのセキュリティ脆弱性があります。コードを見て気づかなかった場合は、ぜひ詳しく学ぶことをおすすめします。
平文パスワードと安全でないハッシュの使用。
アプリケーションのSQLインジェクション脆弱性は、深刻な結果を招くおそれがあります。攻撃者は次のことを実行できます。
機密データへのアクセス:テナント識別子のインジェクションで見たように、ユーザーデータや財務情報などのレコードに不正アクセスします。
データの変更や削除:ここではより多くのデータを取得する方法だけを紹介しましたが、重要なデータの改ざんや削除も、データの完全性を損なう重大なSQLインジェクションのリスクです。
アプリケーションセキュリティの侵害:SQLインジェクションを足がかりとして、他の種類の攻撃を仕掛けます。攻撃者は、より大きなセキュリティ上の影響をもたらすため、複数の脆弱性を組み合わせることがよくあります。
SQLインジェクションを修正する方法
SQLインジェクションは広く見られるセキュリティ脆弱性であり、適切に対処しなければ深刻な結果を招くことを確認しました。ここからは、SQLインジェクションの修正方法を見ていきましょう。
プリペアドステートメントとパラメータ化クエリの使用
SQLインジェクションを防ぐ最も効果的な方法の一つは、SQLクエリの構築にプリペアドステートメントとパラメータ化クエリを使うことです。多くのライブラリやプログラミング言語、プラットフォームで採用されているこれらの手法により、ユーザー入力は実行可能なコードではなくデータとして扱われ、悪意のあるSQLコードの実行を防止できます。
Golangでは、脆弱なアプリケーションで以前は使用しなかったプリペアドステートメントを利用するために、引き続きsqlxパッケージを使用できます。
sqlxを使って安全なSQLクエリを実装する例を紹介します。
package main
import (
"fmt"
"log"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
db, err := sqlx.Connect("sqlite3", "example.db")
if err != nil {
log.Fatalln(err)
}
var user User
username := "john_doe"
query := "SELECT id, name FROM users WHERE name = ?"
err = db.Get(&user, query, username)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
}このGolangコードでは、プレースホルダー文字?を使って、username変数をSQLクエリに安全に挿入しています。これにより、データベースエンジンはその値をSQLクエリの一部ではなく処理対象のデータとして認識し、入力を適切にエスケープします。その結果、GolangでのSQLインジェクション攻撃を防げます。
Snyk Codeを活用してSQLインジェクション脆弱性を検出・修正する
SQLプリペアドステートメントなどのベストプラクティスに従うことは重要ですが、自動化ツールを使えば、見落とす可能性のある脆弱性を検出してコードのセキュリティをさらに強化できます。Snyk Codeは、GolangアプリケーションのSQLインジェクション脆弱性を特定し、修正するのに役立つ強力なツールです。
Snykを開発ワークフローに統合すると、次のようなメリットがあります。
自動スキャン:Snyk CodeはSQLインジェクションを含む脆弱性をソースコードから自動的にスキャンし、実行可能な対策につながる情報を提供します。
継続的な監視:Snykはコードベースに新たな脆弱性がないか継続的に監視し、アプリケーションの安全性を長期にわたって維持します。
開発者に使いやすい:SnykはVS CodeなどのIDEや他の開発ワークフローにシームレスに統合できます。生産性を損なうことなく、開発者がセキュリティをワークフローに取り入れられます。
Snykの強力なセキュリティ機能を活用して、アプリケーションをSQLインジェクション脆弱性から守るには、今すぐSnykの無料アカウントに登録しましょう。Snykを使えば、Golangアプリケーションの安全性を確保し、業界標準への準拠にも役立てられます。
SQLインジェクション脆弱性についてさらに深く学ぶために、次の学習リソースをおすすめします。
Brian VermeerによるSQLインジェクションのチートシート。SQLインジェクション攻撃を防ぐ8つのベストプラクティスを学べます。
Eric SmallingによるGoセキュリティのチートシート