説明
ユーザー入力がシェルのコマンドに入ると、;、&&、| などがコマンドの構文として解釈される場合があります。Goの os/exec は既定ではシェルを起動しませんが、sh -c の呼び出しやシェルの標準入力への書き込みには、このリスクがあります。
想定される影響
- アプリケーションプロセスの権限で、意図しないコマンドを実行されるおそれがあります。
- ファイル、環境変数、認証情報を読み取られたり、データを変更されたりする可能性があります。
- 負荷の高いコマンドでサービスが停止するおそれがあります。管理者権限を取得できるかは、既存のプロセス権限や追加の脆弱性によって異なります。
対処方法
- 標準ライブラリの処理を優先してください。外部プログラムが必要な場合は、シェルを使わず、実行ファイルと引数を分けて
exec.Commandに渡してください。 - 実行ファイル、サブコマンド、オプションはコードに固定し、ユーザー入力を許可した値に制限してください。引数の分離だけでは、対象プログラムの危険なオプションを防げません。
- シェルが不可避な場合は、シェルの種類と挿入位置に応じて各値を引用してください。エスケープだけでは、作業の権限や対象の安全性は検証できません。
- 呼び出し元の権限を確認し、最小権限、時間制限、リソース制限を適用してください。
例
実行ファイルの絶対パスは、配置先の環境に合わせて確認してください。認証・認可、TLS、別途行うリソース分離は省略しています。
変更前
go
package main
import (
"io"
"log"
"net/http"
"os/exec"
)
func unsafeHandler(w http.ResponseWriter, r *http.Request) {
// ユーザーが渡すコマンド文字列(例: name; rm -rf /)
userCmd := r.URL.Query().Get("cmd")
// 変更前: シェルを起動し、ユーザー入力を標準入力へ直接渡す
// ;、&&、| などのメタ文字で追加コマンドを実行できる
c := exec.Command("bash")
stdin, err := c.StdinPipe()
if err != nil {
http.Error(w, "pipe error", http.StatusInternalServerError)
return
}
if err := c.Start(); err != nil {
http.Error(w, "start error", http.StatusInternalServerError)
return
}
io.WriteString(stdin, userCmd+"\n")
stdin.Close()
_ = c.Wait()
w.Write([]byte("done"))
}
func main() {
http.HandleFunc("/run", unsafeHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
変更後
go
package main
import (
"log"
"net/http"
"os/exec"
"regexp"
"time"
"context"
)
// 固定した許可コマンド
var allowed = map[string][]string{
"whoami": {"/usr/bin/whoami"},
"date": {"/bin/date", "+%F %T"},
}
// ユーザーのメッセージを引数にする場合の文字を制限
var safeText = regexp.MustCompile(`^[A-Za-z0-9._:-]{1,64}$`)
func safeHandler(w http.ResponseWriter, r *http.Request) {
action := r.URL.Query().Get("action")
// 1) 許可リストからプログラムと固定引数を選択
cmdDef, ok := allowed[action]
if !ok {
http.Error(w, "invalid action", http.StatusBadRequest)
return
}
// 2) 任意のユーザー入力を引数に加える場合は許可リストで検証
msg := r.URL.Query().Get("msg")
args := []string{}
if msg != "" {
if !safeText.MatchString(msg) {
http.Error(w, "invalid msg", http.StatusBadRequest)
return
}
// 定義済みの printf %s\n <msg> 形式を使う
cmdDef = []string{"/usr/bin/printf", "%s\n", msg}
}
// 3) シェルを使わず引数を分けて渡す
prog := cmdDef[0]
if len(cmdDef) > 1 {
args = cmdDef[1:]
}
// 4) 実行時間を制限
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, prog, args...).CombinedOutput()
if err != nil {
http.Error(w, "exec error", http.StatusInternalServerError)
return
}
w.Write(out)
}
func main() {
http.HandleFunc("/run", safeHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
解説:
- 変更前: ユーザーの文字列を
bashの標準入力へ渡すため、シェルの構文が実行されます。 - 変更後: 固定の実行ファイルと引数を、シェルを介さず渡します。有効な
actionと空ではないmsgがある場合は、固定形式のprintfへ切り替わります。文字の検証は、この限定的なメッセージ用途向けです。2秒のタイムアウトは、メモリ、出力、同時実行数の制限に代わるものではありません。