본문으로 건너뛰기
김신건의 로그

[Rails] 보안: CSRF, XSS, SQL injection, encrypted credentials

· 수정 · 📖 약 1분 · 463자/단어 #rails #security #csrf #xss #encryption
rails security, rails CSRF, strong parameters, encrypted credentials, secret_key_base

정의

Rails는 OWASP Top 10 대부분을 기본 방어한다. 그러나 설정 누락, html_safe 남용, raw SQL 등이 흔한 함정. 보안은 기본 활성이고 명시적으로 우회해야 비활성.

CSRF (Cross-Site Request Forgery)

기본 활성. ApplicationController에 자동 포함:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception    # 5.0+ 기본
end

form_with가 자동 토큰 포함. CSRF 누락 → ActionController::InvalidAuthenticityToken.

AJAX

// 기본 application.js에 포함됨
const csrfToken = document.querySelector("meta[name='csrf-token']").content;

fetch("/api/posts", {
    method: "POST",
    headers: {
        "X-CSRF-Token": csrfToken,
        "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
});

API only

class Api::BaseController < ActionController::API    # CSRF 없음 (API 베이스)
end

# 또는 특정 액션 skip
class WebhooksController < ApplicationController
  skip_forgery_protection only: :stripe
end

세션 인증 + skip은 위험. JWT 등 별도 인증 시만.

XSS

ERB 자동 escape:

<%= user_input %>     <%# 자동 escape #}
<%== user_input %>    <%# raw (위험) %>
<%= raw user_input %>  <%# raw %>
<%= user_input.html_safe %>  <%# raw %>

안전한 HTML 출력

# helper에서
def my_html
  content_tag(:b, user_input)    # 안전
end

def my_html
  "<b>#{user_input}</b>".html_safe   # 위험 (escape 안 함)
end

def my_html
  format_html("<b>{}</b>", user_input)   # 안전 (인자만 escape)
end

sanitize 헬퍼

<%= sanitize(user_html, tags: %w[p br a], attributes: %w[href]) %>

화이트리스트만 허용. 나머지 태그/속성 제거.

SQL Injection

ActiveRecord는 기본적으로 placeholder 사용 → 자동 방어.

# 안전
User.where(email: params[:email])
User.where("email = ?", params[:email])
User.where("email = :email", email: params[:email])

# 위험
User.where("email = '#{params[:email]}'")
User.find_by_sql("SELECT * FROM users WHERE email = '#{params[:email]}'")

문자열 interpolation 금지. placeholder 또는 hash 인자.

order, where with raw

# 위험: column 명을 사용자 입력으로
User.order(params[:sort])    # SQL 인젝션 가능 (5.x)

# 안전: 화이트리스트
allowed = %w[name email created_at]
sort_col = allowed.include?(params[:sort]) ? params[:sort] : "id"
User.order(sort_col)

# 6.0+ Arel
User.order(Arel.sql(sort_col))

Strong Parameters

mass assignment 방어:

def user_params
  params.require(:user).permit(:name, :email, address: [:street, :city])
end

# 위험
User.create(params[:user])    # admin: true 같은 악성 입력 통과

permit된 키만 통과. 컨트롤러마다 명시.

비밀 정보 관리

encrypted credentials (5.2+)

bin/rails credentials:edit
bin/rails credentials:edit --environment production

config/credentials.yml.enc (암호화) + config/master.key (서명 키).

# credentials.yml.enc (편집 시 plain)
secret_key_base: ...
stripe:
  api_key: sk_test_...
  webhook_secret: whsec_...
aws:
  access_key_id: ...
  secret_access_key: ...
Rails.application.credentials.stripe[:api_key]
Rails.application.credentials.dig(:stripe, :api_key)

master.key.gitignore. credentials.yml.enc는 git 커밋 OK.

환경별

bin/rails credentials:edit -e production
# config/credentials/production.yml.enc + production.key

각 환경 다른 키로 분리.

ENV variable

ENV["STRIPE_KEY"]
ENV.fetch("STRIPE_KEY")    # 누락 시 raise

Docker, k8s, CI/CD에서 자연스러움.

HTTPS 강제

# config/environments/production.rb
config.force_ssl = true
config.ssl_options = {
  hsts: { expires: 1.year, subdomains: true, preload: true },
  redirect: { exclude: ->(request) { request.path =~ /healthcheck/ } },
}

자동 HTTPS 리다이렉트 + HSTS 헤더.

헤더 보안

# config/initializers/secure_headers.rb (gem)
SecureHeaders::Configuration.default do |config|
  config.csp = {
    default_src: %w['self'],
    script_src: %w['self' https://cdn.example.com],
    style_src: %w['self' 'unsafe-inline'],
    img_src: %w['self' data: https:],
  }
  config.x_frame_options = "DENY"
  config.x_content_type_options = "nosniff"
  config.referrer_policy = "strict-origin-when-cross-origin"
end

또는 Rails 기본 default_headers:

config.action_dispatch.default_headers = {
  "X-Frame-Options" => "SAMEORIGIN",
  "X-Content-Type-Options" => "nosniff",
  "Referrer-Policy" => "strict-origin-when-cross-origin",
}

Active Record Encryption (7.0+)

class User < ApplicationRecord
  encrypts :ssn, deterministic: true    # 검색 가능
  encrypts :api_key                      # 비결정 (검색 X)
  encrypts :credit_card, downcase: true
end

user.ssn = "..."
user.save
# DB에는 암호화된 값
user.ssn    # 자동 복호화

bin/rails db:encryption:init으로 키 생성.

쿼리:

User.where(ssn: "...")    # deterministic만 가능

비밀번호 (has_secure_password)

class User < ApplicationRecord
  has_secure_password    # password_digest 필드 필요
end

user = User.create(email: "...", password: "secret", password_confirmation: "secret")
user.authenticate("secret")    # User 또는 false

bcrypt 자동. password는 메모리만, DB엔 password_digest만.

인증 (Rails 8 generator)

bin/rails generate authentication

기본적인 로그인/가입/세션 컨트롤러 자동 생성. Devise/Sorcery 같은 풀스택 라이브러리 대안.

자주 보는 패턴

Rate limit

# Rails 8+ 내장
class LoginsController < ApplicationController
  rate_limit to: 5, within: 1.minute, only: :create
end

# 또는 rack-attack gem
Rack::Attack.throttle("logins/ip", limit: 5, period: 60) do |req|
  req.ip if req.path == "/login" && req.post?
end

Audit log

gem "audited"

class Post < ApplicationRecord
  audited
end

post.audits    # 모든 변경 이력

권한 (Pundit)

class PostPolicy < ApplicationPolicy
  def update?
    record.author == user || user.admin?
  end
end

class PostsController < ApplicationController
  def update
    authorize @post
    ...
  end
end

함정

1. RAILS_MASTER_KEY 노출

.env 파일 git 커밋, README에 키 적기 등. 노출 시 즉시 회전:

bin/rails credentials:edit    # 새 키로 재암호화

2. parameter logging 비밀

# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [
  :password, :secret, :token, :api_key, :credit_card,
]

[FILTERED]로 표시. 신규 민감 필드는 직접 추가.

3. CORS 잘못

# Gemfile
gem "rack-cors"
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "*"    # 위험!
    resource "*", methods: [:get, :post, :put, :delete]
  end
end

명시 origin:

origins "https://app.example.com"

4. open redirect

redirect_to params[:redirect_to]    # 위험: //evil.com 가능
redirect_to params[:redirect_to], allow_other_host: false    # 7.0+

5. timing attack

user.api_token == params[:token]    # timing attack 가능
ActiveSupport::SecurityUtils.secure_compare(user.api_token, params[:token])

상수 시간 비교.

보안 도구

  • Brakeman: 정적 분석 (CVE, 코드 패턴)
    gem install brakeman
    brakeman
  • bundler-audit: gem CVE 검사
  • dependabot: GitHub의 의존성 업데이트

CI에 포함.

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기