In this article
Goで安全なコードを書く方法
Goで安全なコードを書くことの重要性について説明します。安全なコードを書くことは、開発者の基本的な責務です。特に、スケーラブルで効率的なシステムの構築に広く使われているGoのような言語では重要です。Goはシンプルで高性能なため、クラウドネイティブアプリケーションで人気があります。Kubernetesエコシステムのツールとの親和性もその理由の一つです。しかし、セキュリティ上の脆弱性は、こうした利点を損なう可能性があります。Goのコードを安全に保つことで、アプリケーションを保護し、ユーザーデータを守り、信頼を維持できます。
Goで書かれたアプリケーションに影響を及ぼす、よくあるセキュリティ上の脆弱性にはどのようなものがあるでしょうか?
Goアプリケーションでよく見られるセキュリティ上の脆弱性
Goアプリケーションは、完全性、機密性、可用性(セキュリティ用語でCIAトライアドとも呼ばれます)を損なうさまざまなセキュリティ上の脆弱性の影響を受ける可能性があります。代表的な脆弱性には、次のようなものがあります。
SQLインジェクションは、信頼できない入力がSQLクエリに連結され、攻撃者がクエリの実行を操作できる状態です。
コマンドインジェクションは、ユーザー入力がシェルコマンドで適切に処理されず、任意のコマンドを実行される状態です。
サーバーサイドリクエストフォージェリ(SSRF)は、アプリケーションがユーザー指定のURLを取得することで、攻撃者が内部サービスにリクエストを送れる可能性がある状態です。
脆弱性の検出と修正におけるSnyk Codeなどのツールの役割
Snyk Codeなどのツールは、Goアプリケーションの脆弱性を検出して修正するうえで重要です。Snyk Codeはソースコードをスキャンしてセキュリティ上の問題を見つけ、実行可能な分析結果と修正方法を提示します。Snykを開発ワークフローに組み込むことで、脆弱性を早期に検出し、セキュリティ侵害のリスクを軽減して、安全なコードベースを維持できます。
Goの例:Snyk CodeによるSQLインジェクションの検出
次のGoコードは、SQLインジェクションに対して脆弱です。
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
_ "github.com/lib/pq"
)
func main() {
http.HandleFunc("/user", func(w http.ResponseWriter, r *http.Request) {
username := r.URL.Query().Get("username")
db, err := sql.Open("postgres", "user=youruser dbname=yourdb sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
rows, err := db.Query(query)
if err != nil {
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
defer rows.Close()
// Process rows...
})
log.Fatal(http.ListenAndServe(":8080", nil))
}この例では、usernameパラメーターがSQLクエリに直接挿入されているため、SQLインジェクションに対して脆弱です。Snyk Codeはこの脆弱性を検出し、パラメーター化クエリを使って防ぐ方法を提案できます。
query := "SELECT * FROM users WHERE username = $1"
rows, err := db.Query(query, username)1. Goでユーザー入力を検証・サニタイズする
入力の検証とサニタイズは、SQLインジェクションやクロスサイトスクリプティング(XSS)など、よくある攻撃ベクトルからGoアプリケーションを守るうえで重要です。クエリパラメーター、HTTPリクエストのJSONレスポンスボディ、データベースの内容など、ユーザー入力を決して信頼してはいけません。入力は常に想定されるスキーマに照らして検証し、必要に応じてサニタイズ処理を適用し、コンテキストに応じた出力エンコードを行いましょう。
Goで入力を検証するためのベストプラクティス
Goでユーザー入力を扱う際は、次のベストプラクティスに従いましょう。
入力スキーマを定義する:厳格なスキーマを使って有効な入力を定義します。
go-playground/validatorなどのライブラリを使うと、これらのルールを適用できます。入力をサニタイズする:ユーザー入力から、危険性のある文字を削除またはエスケープします。たとえば、XSSを防ぐために入力をHTMLエンコードします。これは主に、テンプレートエンジンのツールで役立ち、実装されている手法です。
パラメーター化クエリを使う:文字列を連結するのではなく、パラメーター化クエリを使ってSQLインジェクションを防ぎます。
出力をエンコードする:WebページではHTMLエンコード、APIレスポンスではJSONエンコードなど、コンテキストに応じて出力をエンコードします。
Goの例:SQLインジェクションを防ぐパラメーターバインディング
SQLインジェクションは、ユーザー入力がSQLクエリに直接含まれることで発生する、深刻なアプリケーションレベルのセキュリティ脆弱性です。これを防ぐには、パラメーター化クエリを使います。
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func getUser(db *sql.DB, userID string) (User, error) {
var user User
err := db.QueryRow("SELECT id, name FROM users WHERE id = ?", userID).Scan(&user.ID, &user.Name)
if err != nil {
return user, err
}
return user, nil
}この例では、SQLクエリ内の?プレースホルダーがuserIDパラメーターに安全に置き換えられ、SQLインジェクション攻撃を防ぎます。このセキュリティのベストプラクティスにより、データベースエンジンはクエリ本来の意味と、クエリで使われる値を区別できます。
2. Goでサブシェルの使用を避ける
Goでのサブシェルの使用とは、exec.CommandContext()やexec.Command()などの関数を使ってサブプロセスを起動することです。これらの関数は柔軟性をもたらしますが、特にコマンドインジェクションの脆弱性など、重大なセキュリティリスクも生みます。コマンドインジェクションは、攻撃者が入力データを操作し、ホストシステム上で任意のコマンドを実行することで発生します。不正なデータアクセスやデータの破損、さらにはシステムが深刻に侵害されるおそれがあります。
Goでサブシェルの代わりに使える方法:標準ライブラリAPI、FFIなど
サブシェルに伴うリスクを軽減するため、開発者は外部コマンドが必要になりそうな処理にも、Goの標準ライブラリAPIや外部関数インターフェース(FFI)を優先して使うべきです。Goの標準ライブラリは充実しており、ファイル操作や画像処理などに使える多くのユーティリティが用意されています。その多くはサブシェルの代わりに利用できます。
Goの例:画像処理におけるコマンドインジェクション
ユーザー入力を画像ファイルの処理に使う例を見てみましょう。この例は、コマンドインジェクションにつながるよくある落とし穴を示しています。
package main
import (
"os/exec"
)
func manipulateImage(targetFilename string) error {
convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)
_, err = exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()
}
この例で、targetFilenameがユーザー入力に由来する場合、攻撃者は悪意のあるファイル名を作成して追加のコマンドを注入できます。たとえば、image.png; rm -rf /tmpのようなファイル名は、適切にサニタイズされていなければ破壊的なコマンドを実行する可能性があります。
ここでのリスクは、シェルコマンドの組み立てにユーザー入力を直接使っていることに加え、コマンド引数を適切に組み立てるための仕組みをコードで使っていないことに起因します。このリスクを軽減するには、次の対策を検討してください。
ネイティブライブラリを使う:外部のコマンドラインプログラムに頼るのではなく、Goのネイティブライブラリを使います。
入力を検証・サニタイズする:ユーザー入力が想定されたパターンに合致するよう、必ず検証してサニタイズします。これにより、悪意のある入力が実行されるのを防げます。脆弱なコードの例では、
sh -cを介したシェルへの直接的な埋め込みを避け、コマンドを直接実行する必要があります。ただし、引数インジェクションの脆弱性が生じる可能性もあるため、適切なサニタイズとエスケープが必要です。
3. URLを安全に組み立てる
安全なURLの組み立ては、OWASP Top 10の脆弱性の一つであるサーバーサイドリクエストフォージェリ(SSRF)に直結します。SSRFは、攻撃者がサーバーを操作して、内部リソースや他のプライベートマイクロサービスなど、本来意図されていない宛先にHTTPリクエストを送信させる深刻な脆弱性です。不正な内部サービスへのアクセスやデータの流出、さらにはリモートコード実行につながる可能性があります。SSRFについて詳しく知るには、SSRFに関するSnyk Learnのレッスンをご覧ください。
GoでURLを安全に組み立てる方法
GoでURLを組み立てる際、特にユーザー入力を扱う場合は、SSRFを防ぐための対策を実装することが重要です。ベストプラクティスをいくつかご紹介します。
URLを検証する:ユーザーが指定したURLは、信頼できる承認済みのドメインやパターンの許可リストに照らして必ず検証します。これにより、正当なリクエストのみが送信されます。
URL解析を使う:Goの
net/urlパッケージを使ってURLを解析・検証します。URLを構成要素に分解し、それぞれを確認できます。たとえばこの段階で、対応する特定のプロトコルスキーム(https://)のみを許可し、httpなどを許可しないセキュリティ制御を適用できます。内部ネットワークへのアクセスを制限する:Kubernetesのサイドカー、HTTPプロキシ、APIゲートウェイなど、ネットワークを考慮したセキュリティ制御を適用し、アプリケーションから内部IPアドレスや機密性の高いエンドポイントにリクエストが送られないようにします。
タイムアウトと上限を設定する:SSRFに直接関係する対策ではありませんが、送信リクエストに適切なタイムアウトとレスポンスサイズの上限を設定すると、リソースの枯渇や大量のデータ流出を防ぎ、SSRFの影響を最小限に抑えられます。
Goの例:ユーザー指定URLによるSSRFの脆弱性
次の例は、ユーザーが指定したURLを安全でない方法で処理し、SSRFの脆弱性につながる可能性があるケースを示しています。
package main
import (
"net/http"
)
func fetchUserProfileImage(userURL string) (*http.Response, error) {
// Directly using user input to fetch remote data
resp, err := http.Get(userURL)
return resp, err
}このコードでは、fetchUserProfileImage関数がユーザーからURLを受け取り、それをHTTP GETリクエストに直接使用します。攻撃者はこれを悪用して、内部サービスにリクエストを送信できます。
まとめ
Goで安全なコードを書くには、常に注意を払い、ベストプラクティスを守る必要があります。安全なGo開発のために今回紹介した主なセキュリティ対策は次のとおりです。
Goのコードパスを流れるユーザー入力を検証・サニタイズする。
サブシェルの操作を避け、利用できる場合はGoのネイティブAPIやFFIを優先する。
SSRFの脆弱性を防ぐため、ユーザー入力由来のURLは最大限の警戒をもって扱う。
継続的なセキュリティスキャンにSnyk Codeを活用する
Goアプリケーションのセキュリティを強化するため、次の一歩を踏み出しましょう。今すぐSnykの無料アカウントに登録して、開発ワークフローにセキュリティを取り入れましょう。Snykを活用すれば、自信を持って安全なアプリケーションを構築し、ユーザーを守ることができます。
Goのコードを安全に保つには、Snyk Codeを活用しましょう。継続的なセキュリティスキャンにより、脆弱性が問題になる前にコードベース内で特定し、修正できます。
VS Code IDEでSnyk Codeが動作する様子をご覧ください。機械学習エンジンを使って問題を検出し、Golangでより安全なコードを書くための改善案を提案します。

Fetch the Flag 2026に参加しよう!
2月12日〜13日、東部時間の正午から翌日正午まで開催されるCapture the Flagイベントで、セキュリティスキルを試しましょう。