InfoGrab DocsInfoGrab Docs

보안 코딩 개발 가이드라인

요약

이 문서는 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 같은 간단한 명령으로 응답을 얻어낼 수 있습니다.

영향#

명령 주입의 영향은 명령을 실행하는 사용자 컨텍스트와 데이터의 검증·정제 방식에 따라 크게 달라집니다. 주입된 명령을 실행하는 사용자의 권한이 제한적이면 영향이 낮고, root 사용자로 실행되면 치명적입니다.

발생할 수 있는 영향은 다음과 같습니다.

  • 호스트 머신에서 임의 명령 실행.
  • 시크릿이나 설정 파일에 담긴 비밀번호와 토큰을 포함한 민감 데이터에 대한 무단 접근.
  • /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()

이 코드는 1에 이어 /etc/passwd의 내용을 출력합니다.

아카이브 파일 작업#

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
}

보안 코딩 개발 가이드라인

GitLab v19.5
원문 보기

요약

이 문서는 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 같은 간단한 명령으로 응답을 얻어낼 수 있습니다.

영향#

명령 주입의 영향은 명령을 실행하는 사용자 컨텍스트와 데이터의 검증·정제 방식에 따라 크게 달라집니다. 주입된 명령을 실행하는 사용자의 권한이 제한적이면 영향이 낮고, root 사용자로 실행되면 치명적입니다.

발생할 수 있는 영향은 다음과 같습니다.

  • 호스트 머신에서 임의 명령 실행.
  • 시크릿이나 설정 파일에 담긴 비밀번호와 토큰을 포함한 민감 데이터에 대한 무단 접근.
  • /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()

이 코드는 1에 이어 /etc/passwd의 내용을 출력합니다.

아카이브 파일 작업#

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
}