보안 코딩 개발 가이드라인
이 문서는 GitLab 코드베이스에서 일반적으로 필요한 안전한 Go 프로그래밍 관행에 대한 설명과 가이드라인을 포함합니다. 문자열 리터럴 또는 정규 표현식 리터럴의 문자 앞에 백슬래시가 오면 이스케이프 시퀀스의 일부로 해석됩니다.
이 문서는 GitLab 코드베이스에서 일반적으로 필요한 안전한 Go 프로그래밍 관행에 대한 설명과 가이드라인을 포함합니다. 이 가이드라인은 개발자가 처음부터 안전한 Go 코드를 작성하고, 개발 초기 단계에서 잠재적인 보안 취약점을 식별하며, Go 특유의 모범 사례를 따를 수 있도록 돕기 위한 것입니다. 이러한 표준을 준수함으로써 시간이 지남에 따라 출시되는 보안 취약점의 수를 줄이면서 Go의 내장 보안 기능을 효과적으로 활용하고자 합니다.
정규 표현식 가이드라인#
Go의 이스케이프 시퀀스#
문자열 리터럴 또는 정규 표현식 리터럴의 문자 앞에 백슬래시가 오면 이스케이프 시퀀스의 일부로 해석됩니다. 예를 들어, 문자열 리터럴의 이스케이프 시퀀스 \n은 \와 n 문자가 아닌 단일 newline 문자에 해당합니다.
놀라운 결과를 초래할 수 있는 두 가지 Go 이스케이프 시퀀스가 있습니다. 첫째, regexp.Compile("\a")는 벨 문자와 일치하는 반면, regexp.Compile("\\A")는 텍스트의 시작과 일치하고 regexp.Compile("\\a")는 Go가 아닌 Vim 정규 표현식으로 영문자와 일치합니다. 둘째, regexp.Compile("\b")는 백스페이스와 일치하는 반면, regexp.Compile("\\b")는 단어의 시작과 일치합니다. 이 둘을 혼동하면 예상보다 정규 표현식이 훨씬 더 자주 또는 드물게 통과하거나 실패할 수 있어, 잠재적인 보안 결과를 초래할 수 있습니다.
예시#
다음 예시 코드는 입력 문자열에서 금지된 단어를 확인하는 데 실패합니다:
package main
import "regexp"
func broken(hostNames []byte) string {
var hostRe = regexp.MustCompile("\bforbidden.host.org")
if hostRe.Match(hostNames) {
return "Must not target forbidden.host.org"
} else {
// This will be reached even if hostNames is exactly "forbidden.host.org",
// because the literal backspace is not matched
return ""
}
}
완화 방법#
위의 검사는 작동하지 않지만 백슬래시를 이스케이프하여 수정할 수 있습니다:
package main
import "regexp"
func fixed(hostNames []byte) string {
var hostRe = regexp.MustCompile(`\bforbidden.host.org`)
if hostRe.Match(hostNames) {
return "Must not target forbidden.host.org"
} else {
// hostNames definitely doesn't contain a word "forbidden.host.org", as "\\b"
// is the start-of-word anchor, not a literal backspace.
return ""
}
}
또는 백틱으로 구분된 원시 문자열 리터럴을 사용할 수 있습니다. 예를 들어 regexp.Compile(`hello\bworld`)의 \b는 백틱 내에서 \b가 이스케이프 시퀀스가 아니기 때문에 백스페이스 문자가 아닌 단어 경계와 일치합니다.
경로 순회 가이드라인#
설명#
경로 순회 취약점은 공격자에게 애플리케이션을 실행하는 서버의 임의 디렉토리 및 파일에 대한 액세스를 부여합니다. 이 데이터에는 데이터, 코드 또는 자격 증명이 포함될 수 있습니다.
경로에 디렉토리가 포함될 때 순회가 발생할 수 있습니다. 일반적인 악의적인 예시에는 하나 이상의 ../가 포함되어 파일 시스템에 상위 디렉토리를 보도록 지시합니다. 경로에 여러 개를 제공하면, 예를 들어 ../../../../../../../etc/passwd는 일반적으로 /etc/passwd로 해석됩니다. 파일 시스템이 루트 디렉토리로 돌아가도록 지시받고 더 이상 돌아갈 수 없으면 추가 ../는 무시됩니다. 그런 다음 파일 시스템은 루트에서 조회하여 /etc/passwd를 결과로 반환합니다 - 악의적인 공격자에게 노출되어서는 안 되는 파일입니다!
영향#
경로 순회 공격은 임의 파일 읽기, 원격 코드 실행 또는 정보 노출과 같은 여러 심각한 고위험 이슈를 초래할 수 있습니다.
고려 시점#
사용자 제어 파일 이름/경로 및 파일 시스템 API를 사용할 때.
완화 및 예방#
경로 순회 취약점을 방지하려면 사용자 제어 파일 이름 또는 경로를 처리하기 전에 검증해야 합니다.
- 사용자 입력을 허용된 값의 허용 목록과 비교하거나 허용된 문자만 포함되는지 확인합니다.
- 사용자 제공 입력을 검증한 후 기본 디렉토리에 추가하고 파일 시스템 API를 사용하여 경로를 정규화합니다.
Go는 path.Clean과 관련하여 직관적이지 않은 동작을 합니다. 많은 파일 시스템에서 ../../../../를 사용하면 루트 디렉토리로 이동한다는 점을 기억하십시오. 남은 ../는 무시됩니다. 이 예시는 공격자에게 /etc/passwd에 대한 액세스를 제공할 수 있습니다:
path.Clean("/../../etc/passwd")
// renders the path to "etc/passwd"; the file path is relative to whatever the current directory is
path.Clean("../../etc/passwd")
// renders the path to "../../etc/passwd"; the file path will look back up to two parent directories!
Go의 안전한 파일 작업#
Go 표준 라이브러리는 os.Open, os.ReadFile, os.WriteFile, os.Readlink와 같은 기본 파일 작업을 제공합니다. 그러나 이러한 함수는 사용자 제공 경로가 의도한 디렉토리를 벗어나 민감한 시스템 파일에 액세스할 수 있는 경로 순회 공격을 방지하지 않습니다.
안전하지 않은 사용 예시:
// Vulnerable: user input is directly used in the path
os.Open(filepath.Join("/app/data", userInput))
os.ReadFile(filepath.Join("/app/data", userInput))
os.WriteFile(filepath.Join("/app/data", userInput), []byte("data"), 0644)
os.Readlink(filepath.Join("/app/data", userInput))
이러한 위험을 완화하려면 safeopen 라이브러리 함수를 사용하십시오. 이러한 함수는 보안 루트 디렉토리를 적용하고 파일 경로를 정제합니다:
안전한 사용 예시:
safeopen.OpenBeneath("/app/data", userInput)
safeopen.ReadFileBeneath("/app/data", userInput)
safeopen.WriteFileBeneath("/app/data", []byte("data"), 0644)
safeopen.ReadlinkBeneath("/app/data", userInput)
이점:
- 경로 순회 공격(
../시퀀스)을 방지합니다. - 신뢰할 수 있는 루트 디렉토리로 파일 작업을 제한합니다.
- 무단 파일 읽기, 쓰기 및 심볼릭 링크 해석으로부터 보호합니다.
- 간단하고 개발자 친화적인 대안을 제공합니다.
참고:
OS 명령 주입 가이드라인#
명령 주입은 공격자가 취약한 애플리케이션을 통해 호스트 운영 체제에서 임의 명령을 실행할 수 있는 이슈입니다. 이러한 공격은 사용자에게 항상 피드백을 제공하지 않지만, 공격자는 curl과 같은 간단한 명령을 사용하여 응답을 얻을 수 있습니다.
영향#
명령 주입의 영향은 명령을 실행하는 사용자 컨텍스트와 데이터가 어떻게 검증 및 정제되는지에 따라 크게 달라집니다. 주입된 명령을 실행하는 사용자의 권한이 제한적이어서 영향이 낮은 경우부터 루트 사용자로 실행될 경우 치명적인 영향까지 다양합니다.
잠재적인 영향에는 다음이 포함됩니다:
- 호스트 컴퓨터에서 임의 명령 실행.
- 시크릿 또는 구성 파일의 비밀번호 및 토큰을 포함한 민감한 데이터에 대한 무단 액세스.
- 호스트 컴퓨터의
/etc/passwd/또는/etc/shadow와 같은 민감한 시스템 파일 노출. - 호스트 컴퓨터에 대한 액세스를 통해 관련 시스템 및 서비스 침해.
OS 명령을 실행하는 데 사용되는 사용자 제어 데이터를 다룰 때 명령 주입을 인식하고 이를 방지하기 위한 조치를 취해야 합니다.
완화 및 예방#
OS 명령 주입을 방지하려면 OS 명령 내에서 사용자 제공 데이터를 사용하지 않아야 합니다. 이를 피할 수 없는 경우:
- 허용 목록에 대해 사용자 제공 데이터를 검증합니다.
- 사용자 제공 데이터에 영숫자 문자만 포함되도록 합니다(예: 구문 또는 공백 문자 없음).
- 옵션과 인수를 구분하기 위해 항상
--를 사용합니다.
Go에는 일반적으로 공격자가 OS 명령을 성공적으로 주입하는 것을 방지하는 내장 보호 기능이 있습니다.
다음 예시를 고려하십시오:
package main
import (
"fmt"
"os/exec"
)
func main() {
cmd := exec.Command("echo", "1; cat /etc/passwd")
out, _ := cmd.Output()
fmt.Printf("%s", out)
}
이것은 "1; cat /etc/passwd"를 출력합니다.
내부 보호를 우회하므로 sh를 사용하지 마십시오:
out, _ = exec.Command("sh", "-c", "echo 1 | cat /etc/passwd").Output()
이것은 /etc/passwd의 내용에 이어 1을 출력합니다.
아카이브 파일 작업#
zip, tar, jar, war, cpio, apk, rar, 7z와 같은 아카이브 파일을 작업하는 것은 잠재적으로 심각한 보안 취약점이 애플리케이션에 몰래 들어올 수 있는 영역입니다.
Zip Slip#
2018년, 보안 회사 Snyk은 블로그 게시물을 발표하여 많은 라이브러리 및 애플리케이션에 존재하는 광범위하고 심각한 취약점에 대한 연구를 설명했습니다. 이 취약점은 공격자가 서버 파일 시스템의 임의 파일을 덮어쓸 수 있게 하며, 많은 경우 원격 코드 실행을 달성하는 데 활용될 수 있습니다. 이 취약점은 Zip Slip이라고 명명되었습니다.
Zip Slip 취약점은 애플리케이션이 파일이 추출될 때 파일 위치를 변경하는 디렉토리 순회 시퀀스에 대해 아카이브 내의 파일 이름을 검증하고 정제하지 않고 추출할 때 발생합니다.
악의적인 파일 이름 예시:
../../etc/passwd../../root/.ssh/authorized_keys../../etc/gitlab/gitlab.rb
취약한 애플리케이션이 이러한 파일 이름이 있는 아카이브 파일을 추출하면 공격자는 이러한 파일을 임의 내용으로 덮어쓸 수 있습니다.
안전하지 않은 아카이브 추출 예시#
// unzip INSECURELY extracts source zip file to destination.
func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
os.MkdirAll(dest, 0750)
for _, f := range r.File {
if f.FileInfo().IsDir() { // Skip directories in this example for simplicity.
continue
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
path := filepath.Join(dest, f.Name) // Oops! We blindly use the entry filename for the destination.
os.MkdirAll(filepath.Dir(path), f.Mode())
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, rc); err != nil {
return err
}
}
return nil
}
모범 사례#
경로를 변경할 수 있는 모든 잠재적인 디렉토리 순회 및 기타 시퀀스를 해결하여 대상 파일 경로를 항상 확장하고, 최종 대상 경로가 의도한 대상 디렉토리로 시작하지 않으면 추출을 거부합니다.
LabSec에서 제공하는 보안 아카이브 유틸리티를 사용하는 것이 권장됩니다. 이러한 유틸리티는 Zip Slip 및 기타 유형의 취약점을 처리해줍니다. LabSec 유틸리티는 컨텍스트도 인식하여 추출을 취소하거나 타임아웃하는 것이 가능합니다:
package main
import "gitlab-com/gl-security/appsec/labsec/archive/zip"
func main() {
f, err := os.Open("/tmp/uploaded.zip")
if err != nil {
panic(err)
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
panic(err)
}
if err := zip.Extract(context.Background(), f, fi.Size(), "/tmp/extracted"); err != nil {
panic(err)
}
}
LabSec 유틸리티가 요구 사항에 맞지 않는 경우, Zip Slip 공격으로부터 보호하여 zip 파일을 추출하는 예시가 있습니다:
// unzip extracts source zip file to destination with protection against Zip Slip attacks.
func unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
os.MkdirAll(dest, 0750)
for _, f := range r.File {
if f.FileInfo().IsDir() { // Skip directories in this example for simplicity.
continue
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
path := filepath.Join(dest, f.Name)
// Check for Zip Slip / directory traversal
if !strings.HasPrefix(path, filepath.Clean(dest) + string(os.PathSeparator)) {
return fmt.Errorf("illegal file path: %s", path)
}
os.MkdirAll(filepath.Dir(path), f.Mode())
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, rc); err != nil {
return err
}
}
return nil
}
심볼릭 링크 공격#
심볼릭 링크 공격은 공격자가 취약한 애플리케이션의 서버에서 임의 파일의 내용을 읽을 수 있게 합니다. 원격 코드 실행 및 기타 심각한 취약점을 초래할 수 있는 고위험 취약점이지만, 취약한 애플리케이션이 공격자로부터 아카이브 파일을 수락하고 아카이브 내의 심볼릭 링크에 대한 검증이나 정제 없이 추출된 내용을 어떻게든 공격자에게 다시 표시하는 시나리오에서만 악용 가능합니다.
안전하지 않은 아카이브 심볼릭 링크 추출 예시#
// printZipContents INSECURELY prints contents of files in a zip file.
func printZipContents(src string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
// Loop over each entry and output file contents
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
// Oops! We don't check if the file is actually a symbolic link to a potentially sensitive file.
buf, err := ioutil.ReadAll(rc)
if err != nil {
return err
}
fmt.Println(buf.String())
}
return nil
}
모범 사례#
내용을 읽기 전에 항상 아카이브 항목의 유형을 확인하고 일반 파일이 아닌 항목은 무시합니다. 심볼릭 링크를 반드시 지원해야 하는 경우 아카이브 내의 파일만 가리키도록 하고 다른 곳을 가리키지 않도록 합니다.
LabSec에서 제공하는 보안 아카이브 유틸리티를 사용하는 것이 권장됩니다. 이러한 유틸리티는 Zip Slip 및 심볼릭 링크 취약점을 처리해줍니다. LabSec 유틸리티는 컨텍스트도 인식하여 추출을 취소하거나 타임아웃하는 것이 가능합니다.
LabSec 유틸리티가 요구 사항에 맞지 않는 경우, 심볼릭 링크 공격으로부터 보호하여 zip 파일을 추출하는 예시가 있습니다:
// printZipContents prints contents of files in a zip file with protection against symlink attacks.
func printZipContents(src string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer r.Close()
// Loop over each entry and output file contents
for _, f := range r.File {
if f.FileInfo().IsDir() {
continue
}
// By skipping all irregular file types (including symbolic links), we are sure they can't cause any trouble!
if !zf.Mode().IsRegular() {
continue
}
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
buf, err := ioutil.ReadAll(rc)
if err != nil {
return err
}
fmt.Println(buf.String())
}
return nil
}
