コンテンツにスキップ

認証とセキュリティ

Secret key

session、signed token、OAuth stateに使うsecretはUTF-8で32バイト以上必須です。 repository、image、log、exception responseへ含めず、productionではsecret managerから 読み込みます。

python -c "import secrets; print(secrets.token_urlsafe(48))"

SessionMiddlewareを通るレスポンスには Vary: Cookie を付加し、Cookieごとに キャッシュを分離します。既存のVary指定は保持します。機密情報を保存させたくない レスポンスには、アプリケーション側で Cache-Control: no-store も指定してください。

JSONリクエストの入れ子は128階層までです。上限超過やパーサーの数値制限に 違反する入力は400になります。get_json(silent=True) はこれらの場合も None を返します。

from higuma import SessionMiddleware

app.add_middleware(
    SessionMiddleware,
    os.environ["HIGUMA_SECRET_KEY"],
    cookie_name="higuma_session",
    max_age=14 * 24 * 60 * 60,
    secure=True,
    httponly=True,
    samesite="Lax",
)

cookieは署名されますが暗号化されません。password、token、個人情報をsessionへ保存しないで ください。改変、不正形式、期限切れcookieは空sessionとして扱われます。serialized cookieは 4093 bytesまでです。

session.permanent = Falseのbrowser sessionはMax-Ageを付けず、Trueのsessionだけ persistent cookieになります。login/logout時はsessionをclearして固定化攻撃を防ぎます。

Session login

auth = AuthManager(
    app,
    secret_key=os.environ["HIGUMA_SECRET_KEY"],
    session_options={"secure": True, "samesite": "Lax"},
)


@auth.load_user
def load_user(user_id):
    return find_user(user_id)


@app.get("/me")
@auth.login_required
def me():
    return {"id": current_user.id}

AuthManagerはSessionMiddlewareも登録します。同じappへ別のSessionMiddlewareを 重ねないでください。user loaderはrequestごとに文字列IDを受け、userまたはNoneを返します。 user objectはidとis_authenticatedを持たせます。

  • login_user(user, remember=False): sessionをrotateしてfresh loginにする
  • confirm_login(): 既存loginをfreshに戻す。未loginならUnauthorized
  • logout_user(): sessionをclearする
  • login_required: 未loginを401にする
  • fresh_login_required: non-fresh loginを401にする
  • roles_required / permissions_required: claim不足を403にする

role / permission decoratorはuser objectのroles / permissions iterableを読みます。 match_all=Falseならいずれか一つで許可します。空のrequired claimはValueErrorです。

Password

hasher = PasswordHasher()
stored = hasher.hash(password)

if hasher.verify(password, stored):
    if hasher.needs_rehash(stored):
        stored = hasher.hash(password)

標準形式はrandom salt付きscryptです。verify()は不正formatやresource上限外の値に対して Falseを返します。password hashとparameterはdatabaseへ保存し、plain passwordは保存・log しないでください。

Signed token

signer = TokenSigner(os.environ["HIGUMA_SECRET_KEY"], salt="email-verify")
token = signer.dumps({"user_id": "usr_123"})
payload = signer.loads(token, max_age=900)

用途ごとに異なるsaltを使います。tokenは署名されますが暗号化されません。期限切れ、改変、 不正formatはValueErrorです。

CSRF

cookie sessionを使うbrowser applicationでは、session middlewareの内側に CSRFProtectionを追加します。

app.add_middleware(SessionMiddleware, os.environ["HIGUMA_SECRET_KEY"], secure=True)
app.add_middleware(CSRFProtection)


@app.get("/form")
def form():
    return {"csrf_token": csrf_token()}

POST / PUT / PATCH / DELETEではform field _csrf_token、またはheader X-CSRF-Tokenを送信します。token不一致は403です。custom field_nameを設定した場合、 csrf_token()はactive protectionのfield設定へ追従します。

Rate limit

app.add_middleware(
    RateLimitMiddleware,
    limit=100,
    window=60,
    max_keys=10_000,
)

default keyはrequest.remote_addrです。状態はprocess-local memoryにあり、複数process間で 共有されません。厳密なglobal quotaにはreverse proxyやshared storeを使います。 max_keys到達後の未知keyは共通bucketへ集約され、無制限にmemoryを増やしません。

OAuth 2.0 + PKCE

OAuth flowにはSessionMiddlewareが必須です。stateとPKCE verifierをbrowser sessionへ 束縛し、複数のpending loginを保持しながら各stateを一度だけ消費します。

app.add_middleware(
    SessionMiddleware,
    os.environ["HIGUMA_SECRET_KEY"],
    secure=True,
)

google = OAuth2Client.google(
    client_id=os.environ["GOOGLE_CLIENT_ID"],
    client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
    redirect_uri="https://example.com/auth/google/callback",
    secret_key=os.environ["HIGUMA_SECRET_KEY"],
)


@app.get("/login/google")
def login_google():
    return redirect(google.authorization_url())


@app.get("/auth/google/callback")
def google_callback():
    google.validate_state(request.args["state"])
    token = google.fetch_token(request.args["code"])
    return google.userinfo(token["access_token"])

callbackではtoken交換前にvalidate_state()を呼びます。google、line、discord factoryと custom provider constructorが同じflowを使います。provider responseは1 MiBまでです。 OIDC nonceは送信しますが、higumaはID token signature / claim validatorではありません。

CORS、Host、security header

credential付きCORSはexact originを列挙し、*を使いません。

app.add_middleware(
    CORSMiddleware,
    allow_origins=("https://app.example.com",),
    allow_credentials=True,
)
app.add_middleware(TrustedHostMiddleware, ("example.com", "*.example.com"))
app.add_middleware(
    SecurityHeadersMiddleware,
    strict_transport_security="max-age=31536000; includeSubDomains",
)

CSPはapplicationが実際に読み込むscript、style、image、API originへ合わせて設計してください。 HSTSはHTTPSが全subdomainで継続利用できる場合だけ有効にします。

Reverse proxy

X-Forwarded-ForとX-Forwarded-Protoは標準では無視されます。直接接続元proxyだけを CIDRまたはIPで信頼します。proxyは外部から届いたforwarded headerを必ず上書きします。

app.add_middleware(
    ProxyHeadersMiddleware,
    trusted_proxies=("127.0.0.1", "10.0.0.0/8"),
)

ProxyHeadersMiddlewareを設定せずX-Forwarded-Forを直接読むことは禁止です。 multi-process supervisorのclient IP制約はデプロイを参照してください。

Checklist

秘密鍵は32バイト以上の str または bytes を指定します。整数や配列からの暗黙変換は 拒否します。CSRFトークンの不正な非ASCII入力は403になります。 TrustedHostMiddleware は空のHost、不正なポートやIPv6構文も拒否します。 OAuthのtoken・userinfo HTTPリクエストはリダイレクトを追跡しません。 認証情報の別ホストやHTTPへの転送を防ぐため、プロバイダーの最終HTTPS URLを設定してください。

  • password、session、OAuth token、secretをlogしない
  • HTTPS、secure / httponly cookie、適切なSameSiteを使う
  • credential付きCORSとWebSocket Originをexact matchにする
  • uploadに一意なserver-side name、size/type/quota/scanを適用する
  • raw SQLへuser inputを文字列連結しない
  • debug=False、custom error response、reverse proxy limitを本番で確認する