/docs/security
HTMLの共通設定とセキュリティ
認証やCSPはHonoアプリ側で管理し、言語、nonce、外部iframeの設定を各画面に反映します。
標準で作成されるルートを確認する
標準では、一覧、ビューアー、スライド本体、発表画面、発表者画面、印刷画面が作成されます。これらのルートにhono-decks独自の認証は付きません。外部iframe用の/:slug/embedだけは、embedを指定するまで作成されません。不要な画面はrouter.pagesで無効にしてください。
認証、認可、CSPヘッダーはHonoアプリが担当します。hono-decksは、アプリから渡された言語やnonceを生成するHTMLへ反映します。
Honoで言語を判定し、すべての画面へ渡す
HonoのlanguageDetector()は、既定でクエリ、Cookie、Accept-Languageの順に言語を判定し、c.get("language")から取得できるようにします。
import { languageDetector } from "hono/language"
app.use(languageDetector({
supportedLanguages: ["ja", "en"],
fallbackLanguage: "en",
}))document.langにその値を渡すと、一覧、ビューアー、スライド、印刷、発表画面、発表者画面の<html lang>が揃います。
同じnonceをCSPヘッダーとHTMLへ渡す
nonceはリクエストごとに作り、HonoのVariablesへ保存します。CSPヘッダーに含めた値と、document.nonceが返す値は必ず同じにします。
import { Hono } from "hono"
type AppEnv = {
Variables: {
cspNonce: string
language: string
}
}
const app = new Hono<AppEnv>()
app.use("*", async (c, next) => {
const nonce = crypto.randomUUID()
c.set("cspNonce", nonce)
c.header("Content-Security-Policy", [
"default-src 'self'",
"object-src 'none'",
"base-uri 'self'",
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
"img-src 'self' data: https:",
"frame-src 'self' https://www.youtube.com",
].join("; "))
await next()
})decks.router({
document: {
lang: ({ c }) => c.get("language"),
nonce: ({ c }) => c.get("cspNonce"),
head: ({ surface }) => (
<meta name="hono-decks-surface" content={surface} />
),
},
})hono-decksは指定されたnonceを、パッケージが生成する<style>と<script>へ付けます。YouTube、外部画像、Islandなどを使う場合は、アプリ側のCSPにも必要な配信元を追加してください。
埋め込みを許可するオリジンを指定する
外部サイトにiframeで表示するときだけembedを有効にし、親ページのオリジンを列挙します。
decks.router({
embed: {
frameAncestors: ["https://blog.example.com"],
document: { nonce: ({ c }) => c.get("cspNonce") },
viewer: { controls: false },
},
})公開前に実際のレスポンスを確認する
- 下書きや発表者画面
- 本番で不要なルートが404になることを確認します。
- CSP
- ブラウザの開発者ツールでCSP違反がなく、nonceがヘッダーとHTMLで一致することを確認します。
- 外部埋め込み
- 許可した親サイトでは表示でき、許可していないオリジンでは拒否されることを確認します。