- 참고 자료
- 셸 명령어보다는 File 및 FileUtils 사용
- 항상 설정 가능한 Git 바이너리 경로를 사용하여 Git 명령어 사용
- 명령어를 분리된 토큰으로 나누어 셸 우회하기
- 역따옴표를 사용하지 마세요!
- 경로 문자열의 시작에 사용자 입력을 피하세요
- 경로 트래버설에 대비하기
- 정규 표현식을 문자열의 시작과 끝에 고정시키기
셸 명령어 개발 가이드라인
본 문서에는 GitLab 코드베이스에서 프로세스 및 파일을 다루기 위한 지침이 포함되어 있습니다. 이러한 가이드라인은 코드를 더 신뢰할 수 있고 안전하게 만들기 위한 것입니다.
참고 자료
셸 명령어보다는 File 및 FileUtils 사용
가끔은 Ruby API로도 처리할 수 있는데도 셸을 통해 기본 Unix 명령어를 호출하기도 합니다. 만약 Ruby API가 있다면 Ruby API를 사용하세요.
# 잘못된 예
system "mkdir -p tmp/special/directory"
# 더 나은 예 (분리된 토큰 사용)
system *%W(mkdir -p tmp/special/directory)
# 최상의 예 (셸 명령어를 사용하지 마세요)
FileUtils.mkdir_p "tmp/special/directory"
# 잘못된 예
contents = `cat #{filename}`
# 올바른 예
contents = File.read(filename)
# 가끔은 셸 명령어가 최상의 해결책일 수 있습니다. 아래 예시는 사용자 입력이 없고, Ruby에서 올바르게 구현하기 어려운 예시입니다: /some/path 아래에서 /some/path를 포함하지 않는 120분 이전의 파일과 디렉터리를 모두 삭제합니다.
Gitlab::Popen.popen(%W(find /some/path -not -path /some/path -mmin +120 -delete))
이 코딩 스타일은 CVE-2013-4490을 예방할 수 있었습니다.
항상 설정 가능한 Git 바이너리 경로를 사용하여 Git 명령어 사용
# 잘못된 예
system(*%W(git branch -d -- #{branch_name}))
# 올바른 예
system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name}))
명령어를 분리된 토큰으로 나누어 셸 우회하기
Ruby에게 단일 문자열로 셸 명령어를 전달할 때, Ruby는 /bin/sh
를 통해 전체 문자열을 평가합니다. 본질적으로 우리는 셸에게 한 줄짜리 스크립트를 평가하도록 요청하는 것입니다. 이로 인해 셸 삽입 공격의 위험이 생깁니다. 우리가 스크립팅 기능을 사용하여 작업 디렉터리를 변경하거나 환경 변수를 설정하는 경우가 있습니다. 이 모든 것들은 Ruby에서도 안전하게 직접 수행할 수 있습니다.
# 잘못된 예
system "cd /home/git/gitlab && bundle exec rake db:#{something} RAILS_ENV=production"
# 올바른 예
system({'RAILS_ENV' => 'production'}, *%W(bundle exec rake db:#{something}), chdir: '/home/git/gitlab')
# 잘못된 예
system "touch #{myfile}"
# 더 나은 예
system "touch", myfile
# 최상의 예 (셸 명령어를 아예 실행하지 마세요)
FileUtils.touch myfile
이 코딩 스타일은 CVE-2013-4546을 예방할 수 있었습니다.
:: 코드 블록이 아닌 경우, --
을 사용하여 옵션과 인수를 구분하세요.
옵션과 인수를 --
로 시스템 명령어의 인수 해석기에 명확하게 구분하십시오. 이는 많은 Unix 명령어에서 지원되지만 전부에서 지원되는 것은 아닙니다.
:: 코드 블록이 아닌 경우, --
을 사용하여 옵션과 인수를 구분하기 위한 예시입니다.
# 예시
$ echo hello > -l
$ cat -l
cat: illegal option -- l
usage: cat [-benstuv] [file ...]
위의 예시에서 cat
의 인수 해석기는 -l
을 옵션이라고 가정합니다. 위의 해결책은 cat
에게 -l
이 옵션이 아닌 실제로 인수임을 명확하게 하도록 하는 것입니다. 많은 Unix 명령줄 도구들은 옵션과 인수를 --
로 구분하는 규칙을 따릅니다.
# 잘못된 예
system(*%W(#{Gitlab.config.git.bin_path} branch -d #{branch_name}))
# 올바른 예
system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name}))
이 코딩 스타일은 CVE-2013-4582를 예방할 수 있었습니다.
:: 코드 블록이 아닌 경우,야한 옵션과 인수를 명확하게 구분해주기 위해 --
를 사용함으로써 셸 명령어에 대한 모호함을 피합니다.
역따옴표를 사용하지 마세요!
역따옴표를 사용하여 셸 명령어의 출력을 캡처하는 것은 읽기 쉽지만, 명령어를 셸에 전체 문자열로 전달해야 합니다. 위에서 안전하지 않다고 설명한 바에 따라, GitLab 코드베이스의 주요 해결책은 Gitlab::Popen.popen
을 사용하는 것입니다.
# 잘못된 예
logs = `cd #{repo_dir} && #{Gitlab.config.git.bin_path} log`
# 올바른 예
logs, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} log), repo_dir)
# 잘못된 예
user = `whoami`
# 올바른 예
user, exit_status = Gitlab::Popen.popen(%W(whoami))
GitLab Shell과 같은 다른 저장소에서는 IO.popen
을 사용할 수도 있습니다.
# 안전한 IO.popen 예시
logs = IO.popen(%W(#{Gitlab.config.git.bin_path} log), chdir: repo_dir) { |p| p.read }
:: 주의: Gitlab::Popen.popen
과 달리, IO.popen
은 표준 오류를 캡처하지 않습니다.
경로 문자열의 시작에 사용자 입력을 피하세요
루비에서 파일을 열고 읽는 여러 가지 방법은 파일의 표준 출력을 읽는 대신 프로세스의 표준 출력을 읽는 데 사용될 수 있습니다. 다음의 두 명령어는 대략적으로 같은 동작을 합니다:
`touch /tmp/pawned-by-backticks`
File.read('|touch /tmp/pawned-by-file-read')
핵심은 |
로 시작하는 ‘파일’을 열 수 있는 것입니다. 영향을 받는 메서드에는 Kernel#open, File::read, File::open, IO::open 및 IO::read가 포함됩니다.
open
및 read
의 이러한 동작을 피하기 위해, 열려는 파일 이름의 시작을 공격자가 제어하지 못하도록 하는 것이 중요합니다. 예를 들어, 아래는 실수로 |
로 시작하는 셸 명령어를 시작하는 것을 방지하는 데 충분합니다.
# 우리는 repo_path가 공격자(사용자)에 의해 제어되지 않았다고 가정합니다.
path = File.join(repo_path, user_input)
# 이제 path는 `|`로 시작할 수 없습니다.
File.read(path)
사용자 입력이 상대 경로를 사용해야 하는 경우, 경로에 ./
를 접두어로 붙입니다.
사용자가 제공한 경로에 접두어를 붙이는 것은 -
로 시작하는 경로에 대해 추가적인 보호 기능을 제공합니다(위의 --
사용에 대한 토론을 참조하세요).
경로 트래버설에 대비하기
경로 트래버설은 프로그램(GitLab)이 디스크의 특정 디렉토리에 대한 사용자 액세스를 제한하려고 시도하지만 사용자가 ../
경로 표기를 이용하여 해당 디렉토리 외부의 파일을 열어버리는 보안 이슈입니다.
# 사용자가 경로를 제공하고 우리를 속이려고 하는 경우
user_input = '../other-repo.git/other-file'
# 어딘가에서 저장소 경로를 찾아봅니다
repo_path = 'repositories/user-repo.git'
# 아래 코드의 의도는 repo_path 아래의 파일을 열려는 것이지만
# 사용자가 '..'를 사용하여 'repositories/other-repo.git'로 '탈출'할 수 있습니다.
full_path = File.join(repo_path, user_input)
File.open(full_path) do # 이런!
이를 방지하는 좋은 방법은 Ruby의 File.absolute_path
에 따라 전체 경로와 ‘절대 경로’를 비교하는 것입니다.
full_path = File.join(repo_path, user_input)
if full_path != File.absolute_path(full_path)
raise "잘못된 경로: #{full_path.inspect}"
end
File.open(full_path) do # 등.
이와 같은 확인은 CVE-2013-4583를 방지했을 수 있었습니다.
정규 표현식을 문자열의 시작과 끝에 고정시키기
쉘 명령에 전달되는 사용자 입력을 유효성 검사하기 위해 정규 표현식을 사용할 때, 문자열의 시작과 끝을 나타내는 \A
및 \z
앵커를 사용하여 ^
및 $
또는 어떠한 앵커도 사용하지 말아야 합니다.
사용하지 않으면 공격자가 이를 사용하여 잠재적으로 유해한 효과를 가진 명령을 실행할 수 있습니다.
예를 들어, 프로젝트의 import_url
이 아래와 같이 유효성을 검사하는 경우, 사용자는 로컬 파일 시스템의 Git 리포지토리로부터 GitLab을 속일 수 있습니다.
validates :import_url, format: { with: URI.regexp(%w(ssh git http https)) }
# URI.regexp(%w(ssh git http https))은 대략 /(ssh|git|http|https):(URL처럼 보이는_무언가)/로 평가됩니다
사용자가 다음과 같이 import URL로 제출하는 경우를 가정해봅시다.
file://git:/tmp/lol
사용된 정규 표현식에 앵커가 없기 때문에 값의 git:/tmp/lol
이 일치하여 유효성 검사를 통과합니다.
GitLab이 가져올 때, 다음 명령을 실행하여 import_url
을 인수로 전달합니다.
git clone file://git:/tmp/lol
Git은 git:
부분을 무시하고 경로를 file:///tmp/lol
로 해석하고 새 프로젝트로 리포지토리를 가져옵니다. 이 작업은 공격자에게 시스템의 어떤 리포지토리든, 비공개든 상관없이 액세스 권한을 부여할 수 있습니다.