[TIL] 정적 보안 분석과 gosec

gosec은 go를 위한 정적 보안 분석 도구이다. 널리 알려진 취약점들을 대비할 수 있게 코드를 분석해주는 도구이다. gosec이라는 명령어를 통해 코드를 분석하고 레포트를 생성할 수 있다.

설치 및 활용 예

설치방법 링크 를 참고.

여기는 바로 사용할 수 있도록 간단히 정리한다.

최신버전 설치방법:

# 아래 코드를 실행하면 최신 버전의 gosec을 설치하며 $(go env GOPATH)/bin/gosec 에 바이너리가 설치된다.
curl -sfL https://raw.githubusercontent.com/securego/gosec/master/install.sh | sh -s -- -b $(go env GOPATH)/bin latest

다음은 정적 보안분석을 쉽게 실행할 수 있게 Makefile로 만들어 관리하는 예시이다.

gosec이 설치되어 있어야 동작한다.

# 정적 보안 분석 사용 예시
sast:
	@mkdir -p .public/sast # .public/sast 디렉토리 생성
	@gosec -fmt=html -out=.public/sast/index.html ./...; gosec -fmt=json -out=.public/sast/results.json ./...;
.PHONY: sast

참고로 ./… 는 재귀적으로 현재 디렉토리부터 모든 하위 디렉토리를 검사한다.

make sast를 실행하면 html과 json 파일이 .public/sast에 생성된다.

간단하게는 아래 명령만으로도 가능하다.

 gosec ./...

대표적인 정적 보안 분석 예시

securego/gosec에서 제공하는 문서에서 간단히 정리해 봤다.

참고문서

하드코딩된 비밀정보

potential hardcoded credentials

func main() {
  password := "87abc123*af$3"
}

위 코드처럼 코드 내에 비밀번호를 직접 입력해두는 것은 위험하다. gosec은 이런 실수를 잡아준다.

모든 네트워크 인터페이스에 바인딩

네트워크에서 0.0.0.0은 모든 IP에서 접근 가능함을 의미한다.

참고) 127.0.0.1과 0.0.0.0의 차이

따라서 보안 이슈가 있을 수 있고, 이 경우, gosec은 에러를 띄워준다.

package main

import (
	"log"
	"net"
)

func main() {
	l, err := net.Listen("tcp", "0.0.0.0:2000")
	if err != nil {
		log.Fatal(err)
	}
	defer l.Close()
}

“0.0.0.0”으로 네트워크가 열린 것을 감지해 에러를 띄워주는 구조이다.

unsafe package

unsafe 패키지를 이용하면 low level 에서 메모리 관리를 할 수 있지만, 보안 취약점을 노출하기도 쉬워진다.

  • data leak
  • 메모리 오염
  • 공격자의 스크립트 실행

위 세가지 이슈가 발생할 가능성이 생긴다.

체크되지 않은 error

아래 main함수처럼 실행한 함수에서 리턴된 error를 적절히 핸들링 해주지 않는 것도 분석 과정에서 검출된다.

package main
import "fmt"
func test() (int,error) {
    return 0, nil
}

func main() {
    v, _ := test() // return된 에러를 _로 받아 무시하고 있음.
    fmt.Println(v)
}

URL을 변수에 담거나 오염될 수 있는 input에서 가져오는 경우

URL을 상수가 아닌 변수로 선언할 경우, 혹은 사용자의 input처럼 위협소지가 있는 것에서 가져올 경우 SSRF 공격 등에 취약할 수 있다.

format string이나 string concatenation을 이용해 query문을 작성하는 경우

제목은 어렵지만 예시는 쉽다.

func main() {
  // format string
  formatString := fmt.Sprintf("SELECT * FROM users where name = '%s'", variable)

  // string concatenation
  stringConcat := "SELECT * FROM users WHERE gender = " + variable
}

이런 경우 SQL Injection에 취약해지게 된다.

대안은 크게 2가지가 제시된다.

  1. 상수로 쿼리문을 작성해두고 활용
  2. database/sql 패키지의 활용
// 상수 활용
const staticQuery = "SELECT * FROM users WHERE age = 10"

// database/sql
import "database/sql"

func main() {
  db, err := sql.Open("sqlite3", ":memory:")
  if err != nil {
    panic(err)
  }
  rows, err := db.Query("SELECT * FROM users WHERE name = ?", name)
}

이렇게 외부 라이브러리의 argument placeholder를 사용하게 되면 적절한 escape가 자동으로 제공되기 때문에 안전해진다.

오염될 수 있는 input으로 file path를 받는 경우

  1. 변수에 file path를 할당하는 경우,
  2. user input 등으로 받는 경우

아래와 같이 /safe/path에서 뒤로 탐색하며 /private/path에 접근할 수 있다.

func main() {
  repoFile := "/safe/path/../../private/path"
}

자세한 내용은 링크 참조.

결론

물론 gosec이 완벽하지는 않다. 사용 사례에 따라 ‘이걸 왜 검출 못하지’ 하는 부분도 분명히 있기 때문에 너무 과신해서는 안된다. 하지만 프로그래머는 완벽하지 않기 때문에 이런 정적 분석 도구가 유용한 경우가 있다.

좀 더 세팅을 만져가며 본인의 상황에 맞게 설정할 필요가 있을 것 같다. 그렇게 쓸 경우 더욱 강력해질 것 같다.