Anatomi Kerentanan Session & Auth di Hanami

Pengamanan autentikasi berbasis session pada Hanami 3.0 bertumpu pada dua layer utama: proteksi state transport melalui Rack session cookie dan integritas payload melalui contract schema. Mengabaikan konfigurasi cookie mengekspos session ID terhadap serangan Cross-Site Scripting (XSS) dan Cross-Site Request Forgery (CSRF). Di sisi lain, parsing parameter yang longgar memicu eksploitasi parameter tampering.

1. Hardening Rack Session Middleware

Hanami memanfaatkan ekosistem Rack untuk manajemen session. Konfigurasi session cookie dilakukan di level slice atau base app (config/app.rb). Kunci keamanan cookie meliputi tiga flag: HttpOnly untuk mencegah akses JavaScript, Secure untuk membatasi cookie hanya pada HTTPS, dan SameSite untuk mencegah pengiriman cookie pada cross-site requests.

# config/app.rb
module MyApp
  class App < Hanami::App
    config.actions.sessions = :cookie, {
      key: "_my_app_session",
      secret: settings.session_secret,
      expire_after: 60 * 60 * 24, # 24 jam
      path: "/",
      httponly: true,
      secure: Hanami.env?(:production),
      same_site: :lax
    }
  end
end

Gunakan same_site: :strict jika aplikasi Anda adalah portal backend tertutup tanpa integrasi tautan masuk dari domain pihak ketiga. Gunakan :lax jika user memerlukan session tetap aktif saat mengklik tautan eksternal langsung ke aplikasi.

2. Sanitasi dan Pengelolaan Secrets Runtime

Hanami 3.0 mengelola konfigurasi runtime via dynamic setting contracts (config/settings.rb). Hindari membaca ENV langsung di dalam controller atau action untuk mencegah secret leakage saat debugging atau unhandled exception.

# config/settings.rb
module MyApp
  class Settings < Hanami::Settings
    setting :session_secret, constructor: Types::String
  end
end
Pastikan SESSION_SECRET memiliki entropi minimal 64 karakter acak. Nilai dapat di-generate dengan command bundle exec hanami secret atau openssl rand -hex 32.

3. Validasi Schema Payload Autentikasi

Validasi input login harus ditangani langsung pada boundary action sebelum menyentuh domain auth. Hanami Actions terintegrasi dengan dry-validation/dry-schema untuk casting tipe data dan validasi boundary secara otomatis.

# slices/main/actions/sessions/create.rb
module Main
  module Actions
    module Sessions
      class Create < Main::Action
        params do
          required(:email).filled(:string, format?: /\A[^@\s]+@[^@\s]+\z/)
          required(:password).filled(:string, min_size?: 12)
        end

        def handle(req, res)
          halt 422 unless req.params.valid?

          # ponytail: validasi credential langsung tanpa abstraksi domain service tambahan
          user = Authenticator.call(req.params[:email], req.params[:password])
          halt 401 unless user

          # Regenerasi session ID untuk mitigasi Session Fixation
          res.session.clear
          res.session[:user_id] = user.id
          res.redirect_to "/dashboard"
        end
      end
    end
  end
end

4. Verifikasi Keamanan via Assert Test

Gunakan testing berbasis Rack/Hanami::Action untuk memastikan payload invalid ditolak dan header Set-Cookie membawa flag proteksi yang diwajibkan.

# test/actions/sessions/create_test.rb
require "test_helper"

class CreateSessionTest < Minitest::Test
  def test_rejects_invalid_email_format
    action = Main::Actions::Sessions::Create.new
    response = action.call(email: "invalid-format", password: "short")

    assert_equal 422, response.status
    refute response.params.valid?
    assert response.params.errors.key?(:email)
    assert response.params.errors.key?(:password)
  end

  def test_cookie_flags_in_rack_response
    app = MyApp::App.rack_app
    env = Rack::MockRequest.env_for("/sessions", method: "POST", params: {
      email: "[email protected]",
      password: "SecurePassword123!"
    })
    
    status, headers, _body = app.call(env)
    cookie_header = headers["set-cookie"] || headers["Set-Cookie"]

    if cookie_header
      assert_includes cookie_header, "HttpOnly"
      assert_includes cookie_header, "SameSite=Lax"
    end
  end
end

Trade-offs dan Tips Operasional

  • Session Cookie Size: Cookie store Rack memiliki batasan ukuran 4KB. Jangan menyimpan state besar di session; simpan hanya identifier integer/UUID primitif.
  • Subdomain Sharing: Jika session harus shared antar-subdomain, eksplisitkan opsi domain: :all pada konfigurasi cookies, namun batasi scope domain seminimal mungkin.
  • Handling Invalidation: res.session.clear hanya menghapus key pada cookie yang dibawa request. Jika state persistence di sisi server dibutuhkan (misal: single sign-out terpusat), gunakan storage Redis atau cache store sebagai pengganti cookie session murni.