설명
사용자 입력이 셸 명령문에 들어가면 ;, &&, | 같은 문자가 명령 문법으로 해석될 수 있습니다. 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")
// BAD: 쉘을 띄우고 표준입력으로 사용자 입력을 그대로 전달
// 사용자는 ;, &&, | 같은 메타문자를 섞어 추가 명령을 실행시킬 수 있음
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초 타임아웃은 별도 메모리·출력·동시 실행 제한을 대신하지 않습니다.