- 참조
- 쉘 명령어 대신 File 및 FileUtils 사용
- 항상 설정 가능한 Git 바이너리 경로를 사용하여 Git 명령어 실행
- 명령어를 별도의 토큰으로 분할하여 쉘 우회하기
- 역 따옴표 사용 금지
- 경로 문자열 시작 부분에 사용자 입력 사용 금지
- 경로 순회에 대한 보안
- 정규 표현식을 문자열의 시작과 끝에 적절히 고정시키세요
쉘 명령어 개발 가이드라인
이 문서에는 GitLab 코드베이스에서 프로세스 및 파일을 다루는 지침이 포함되어 있습니다. 이러한 지침은 코드를 더 안정적이고 안전하게 만드는 데 목적이 있습니다.
참조
- Google Ruby Security Reviewer’s Guide
- OWASP Command Injection
- Ruby on Rails Security Guide Command Line Injection
쉘 명령어 대신 File 및 FileUtils 사용
가끔은 우리가 쉘을 통해 기본 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 하위에서 120분보다 오래된 모든 파일과 디렉터리를 삭제하지만 /some/path 자체는 삭제하지 않습니다.
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 명령어에서 지원되지만 모두에서는 지원되지 않습니다.
GitLab 코드베이스에서는 이 혼란을 항상 지원하는 명령에 대해 --
를 사용하여 옵션과 인수를 구분합니다.
# 잘못된 예
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'
# 아래 코드의 의도는 리포지터리 경로 아래의 파일을 열려는 것입니다. 그러나
# 사용자가 '..'을 사용하여 '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
이 아래와 같이 유효성을 검사할 때, 사용자가 GitLab을 속여 로컬 파일 시스템의 Git 리포지터리로부터 클론하도록 할 수 있습니다.
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
로 해석하여 새 프로젝트로 리포지터리를 가져옵니다. 이 동작은 공격자에게 시스템 내의 모든 리포지터리에 액세스할 수 있는 기회를 제공할 수 있습니다. 그리고 해당 리포지터리가 비공개인지 여부에 상관없이 해당 가능성이 있습니다.