「俺のhelmet」がHonoのBuilt-in Middlewareになるということ
Honoのミドルウェアは作れる
Honoには様々なミドルウェアがある。公式のsrc/middleware
やhonojs/middleware
リポジトリにたくさんある。ということはHonoのミドルウェア、俺も自作できるんや…!と気づいたので、何か作ろうとおもった。
似ている位置づけのExpressのミドルウェアを調べていたところ、「helmet」が便利そうであることがわかった。
Helmet
helmetとはExpressのミドルウェアで、こんな風に書くとセキュリティ系のヘッダーをいい感じにつけてくれる。
import express from "express";
import helmet from "helmet";
const app = express();
// Use Helmet!
app.use(helmet());
app.get("/", (req, res) => {
res.send("Hello world!");
});
app.listen(8000);
Defaultでも透過的にもつけてくれるし、必要なとこは柔軟にカスタムできたり、要らんものを外したりなどと、かゆいとこに手が届くものがミドルウェアだ。これ、Honoにも欲しいな!
ミドルウェアを作ろう
方針
ここで2つの方針がある
- Express用のHelmetミドルウェアに対してのAdaptorを作る。
- 本家を参考に新しく作り直す
結構悩んだのだけど、その時の結論としては、他のFlamework(Express)のミドルウェアに依存するのは違うなーという気持ちになっていた。
いうてheaderをつけたり外したりするだけだし、行けるべ!と舐めたテンションだったのもまた事実……(後で苦しむ)
試作
とりあえずExpressのドキュメントと、Honoのミドルウェアを参考にChatGPTで下書きを作って、色々直していく。
とりあえず作ってみたら、とにかく便利に感じた。
import { secureHeaders } from 'hono/secure-headers'
const app = new Hono()
app.use('*', secureHeaders())
でベースラインを作れるの便利じゃない? 使わない設定はこうするだけだし。
const app = new Hono()
app.use(
'*',
secureHeaders({
xFrameOptions: false,
xXssProtection: false,
})
)
このようなシンプルかつ強力なミドルウェアは、私のようなセキュリティ不得手な人間ほど嬉しいのでは?と確信し、公開することとした。
方針_r2
さて、どこに公開しよう。
前提としてHonoのミドルウェアは以下の3種類がある。
- Built-in Middleware:
honojs/hono
リポジトリに標準のミドルウェアとして配置する - Third-party Middleware:
honojs/middleware
リポジトリに3rd partyミドルウェアとして配置する - Custom Middleware: 自分のリポジトリで独自のミドルウェアとして公開する
最初はCustom Middlewareのつもりだったが、Third-party Middlewareは比較的間口が広くどの開発者でも配置できそうなので、案2を選ぼうとした。が、改めて読むとこのリポジトリは「3rd Partyライブラリに依存するミドルウェア」が中心であることに気づいた。
それと、ここまでの段階では真面目に本家のissueを探さずに作っていたのだが、たまたまこのIssueを見つけた。
あれ、普通に需要あったんじゃん? これ1として置けるんじゃないか?と気づいたのでPullRequestを出した(ダメなら2. か3. にするのは大した手間ではないし)
これは受け入れられ、あれよあれよという間にマージされた。
設計してつくる
話が飛びすぎてしまったので、もう少し設計のころの話もしよう。
helmetのミドルウェア自体の機能を完コピする、というのはやめた。大仰だしまずは雑に動いて、まあまあ便利というところを目指した。
具体的にはAPIによる細かい操作とかは考えずに、
- default: helmetのデフォルト設定
- disable: 該当ヘッダ設定はしない
- CSP:
面倒なので一旦未実装
だけを実装する動きで作った。要はhelmetのほとんどのデフォルト値を透過的に設定し、不要なものは無効化してくれ(自分で設定してくれ)という割り切りだ。
PullRequest
実際にPRを出すと、「むしろCSPは作って欲しい」をはじめ他にもいくつかの要望を見ていた人から貰った。
自分用に作ってた時は結構こだわって割り切り仕様にしていたのだけど、不思議と嫌だなという気持ちはなくて淡々と作れた。”無我の境地”や”OSS精神”、などカッコいい言い方はある気もするが、「もう俺1人のコードじゃないしな」というのが1番リアルな表現だ。良し悪しは意見するけど、それ以上のこだわりは無いし(嫌になったらフォークしてまた作ればいいし)、肩の荷が降りたというところかもしれない。
新生Helmet = SecureHeaders のいま
今はここから進んで、CSPやreport-toを設定できます!
const app = new Hono()
app.use(
'/test',
secureHeaders({
reportingEndpoints: [
{
name: 'endpoint-1',
url: 'https://example.com/reports',
},
],
// -- or alternatively
// reportTo: [
// {
// group: 'endpoint-1',
// max_age: 10886400,
// endpoints: [{ url: 'https://example.com/reports' }],
// },
// ],
contentSecurityPolicy: {
defaultSrc: ["'self'"],
baseUri: ["'self'"],
childSrc: ["'self'"],
connectSrc: ["'self'"],
fontSrc: ["'self'", 'https:', 'data:'],
formAction: ["'self'"],
frameAncestors: ["'self'"],
frameSrc: ["'self'"],
imgSrc: ["'self'", 'data:'],
manifestSrc: ["'self'"],
mediaSrc: ["'self'"],
objectSrc: ["'none'"],
reportTo: 'endpoint-1',
sandbox: ['allow-same-origin', 'allow-scripts'],
scriptSrc: ["'self'"],
scriptSrcAttr: ["'none'"],
scriptSrcElem: ["'self'"],
styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
styleSrcAttr: ['none'],
styleSrcElem: ["'self'", 'https:', "'unsafe-inline'"],
upgradeInsecureRequests: [],
workerSrc: ["'self'"],
},
})
)
true
(default設定), false
(disable)以外に、文字列で直接上書きできたりします。
const app = new Hono()
app.use(
'*',
secureHeaders({
strictTransportSecurity: 'max-age=63072000; includeSubDomains; preload',
xFrameOptions: 'DENY',
xXssProtection: '1',
})
)
カスタマイズ性とやり過ぎ感の無さがいいところに落ち着いたなーと個人的には思っている。
まとめ
というわけであなたのHonoアプリケーションでもimport { secureHeaders } from 'hono/secure-headers'
をして使ってみてください。
「俺のhelmet」改め、SecureHeadersを挟むだけでセキュリティ系がヘッダが簡単に付与でき、安全に近づきます。
Discussion