これは はてなエンジニア Advent Calendar 2025 1日目の記事です。
はてなでフロントエンドエキスパートをしている
id:mizdra です。普段は JavaScript を書いてて、趣味で色々なツールを作ってます。
ところで最近、npmjs.com へのサプライチェーン攻撃が話題ですね。以前から npmjs.com ではサプライチェーン攻撃が発生していましたが、今年は規模が大きいものが頻繁に発生しています。
マルウェアをインストールしない・実行しないことが一番ではあるのですが、昨今の npmjs.com への攻撃の様子を踏まえると、そうも言っていられません。そのため、何個も保護レイヤーを設ける「多層防御」という考えが重要です。マルウェアをインストールしない・実行しないための対策とは別に、仮にマルウェアが実行されても、その影響を抑える対策があると良いでしょう。
最も手軽な対策は、ファイルシステム上に機密情報を置かないことです。具体的には、以下のようなものです。
- リポジトリのディレクトリに API トークンなどを置かない
- CLI ツール (npm, aws, …) の無期限な認証トークンを、ファイルシステムに置かない
- SSH 鍵もファイルシステムに置かない
どれも比較的無理なくできるものなので、ぜひやってみると良いでしょう。
一方で、Sandbox を利用するという対策もあります。
- Development Container (a.k.a. Dev Container) を使う
- Docker コンテナの中で開発する
- ブラウザベースの開発環境を使う
- GitHub Codespaces, Stackblitz, vscode.dev など
Sandbox 内には、そのリポジトリのファイルしか mount されません。そのため、マルウェアの影響をそのリポジトリの中だけに限定できます *1。
Sandbox を使った対策の欠点
Sandbox は便利な反面、導入が面倒な技術です。例えば、ホスト側からポートにアクセスできるよう明示的な許可が必要です。ボリュームの mount の設定も必要です。Development Container の場合は、中でシェルを起動して作業できるようになってますから、そのシェルから使う ツール類の設定なども必要でしょう。場合によっては、アプリケーション側のコードの改修も必要かもしれません。
Sandbox は導入と運用にそれなりのコストが掛かってしまいます。マルウェアに対する良い対策だと分かっていても、中々導入は難しいのです。
「restricted shell」の紹介
とはいえ、やはり Sandbox 相当のものは欲しいです。そこで Sandbox と同じような効果が得られて、もっと手軽に使えるものを作ってみました。それが「restricted shell」です。はじめに言っておきますが、macOS 限定、かつ ghq でリポジトリを管理しているユーザ限定です。

リポジトリを cd すると、restricted shell という制限の掛かったサブシェルが起動します。その中では今いるリポジトリから他のリポジトリのファイルを読み取れなくなります。
他のリポジトリのファイルの読み取りが禁止されるだけで、それ以外の制限はありません。そのため、以下のようなことができます。
npm installできる- ネットワークアクセスは自由
^Rでシェルの履歴が見れる- ホームディレクトリ配下のファイルは読み取り制限なし
- プロセスも起動し放題
Sandbox というよりは、非常に緩い保護レイヤーのようなものです。普段使いできるように、どのリポジトリでも使えるように、意図的に緩い制限にしています。
とはいえ制限を解除して作業したいこともあるでしょう。そこで脱出ハッチとして exit で restricted shell を終了できるようにしてます。また、rsh で再度 restricted shell を起動することも可能です。

あとサブシェル (restricted shell) から抜ける時に、サブシェルの履歴とカレントディレクトリを親シェルに引き継ぐようにしてます。そのおかげで、restricted shell を終了してもシームレスに作業を継続できます。
普段使いするものなので、使い勝手には拘って設計してます。
仕組み
Apple Seatbelt という技術を使っています。macOS に搭載されているセキュリティ保護機能で、ルールベースでプロセスに制限をかけられます。
今回はこの Apple Seatbelt で、特定のディレクトリへのアクセスが制限されたサブシェルを立ち上げています。Apple Seatbelt による制限は、親プロセスから子プロセスへと継承されます。そのため、サブシェル内から起動されるあらゆるプロセス (ls .. など) が制限を継承し、ls .. などがエラーでコケるようになります。
実装
実装は Apple Seatbelt の設定ファイル、.zshrc、プロンプトの設定 (starship.toml) に分かれています。まず Apple Seatbelt の設定ファイルから。
;; .config/sandbox/restricted-shell.sb (version 1) (allow default) (deny file-read* (subpath (param "GHQ_ROOT")) ) (allow file-read* (subpath (param "REPOSITORY")) ) ;; starship がリポジトリの親ディレクトリのメタデータを読みに行くようなので、例外的に許可。 ;; 本当はもうちょっと厳しくしたい。 (allow file-read-metadata (subpath (param "GHQ_ROOT")) )
REPOSITORY が今 cd してるリポジトリのパスを表すパラメータで、GHQ_ROOT が ghq のルートディレクトリを表すパラメータです。一括で GHQ_ROOT の読み取りを禁止し、その上で REPOSITORY を例外的に許可してます。
.zshrc は以下の通り。
PREV_REPOSITORY="" function start-restricted-shell-if-needed() { local ghq_root="$(ghq root)" local repository="" [[ "$PWD" =~ ^$ghq_root/[^/]+/[^/]+/[^/]+ ]] && repository="$MATCH" if [[ -z "$repository" ]]; then PREV_REPOSITORY="" elif [[ "$RESTRICTED_SHELL" != "1" && "$repository" != "$PREV_REPOSITORY" ]]; then PREV_REPOSITORY="$repository" echo "info: Start a restricted shell. (To exit, type 'exit'.)" RESTRICTED_SHELL=1 sandbox-exec -f ~/.config/sandbox/restricted-shell.sb -D GHQ_ROOT="$ghq_root" -D REPOSITORY="$repository" "$SHELL" fc -R cd "$(</tmp/rsh-pwd)" fi } function restricted-shell-exit() { if [[ "$RESTRICTED_SHELL" == "1" ]]; then pwd > /tmp/rsh-pwd fi } autoload -Uz add-zsh-hook add-zsh-hook precmd start-restricted-shell-if-needed add-zsh-hook zshexit restricted-shell-exit alias rsh='PREV_REPOSITORY="" start-restricted-shell-if-needed'
色々書いてありますが、重要なのは add-zsh-hook precmd ... の部分と、start-restricted-shell-if-needed 関数の中身です。zsh の precmd hook を使い、プロンプトが表示される際にリポジトリのディレクトリかどうかチェックしてます。そして、リポジトリのディレクトリなら RESTRICTED_SHELL=1 sandbox-exec -f ~/.config/sandbox/restricted-shell.sb ... $SHELL でサブシェルを起動してます。コードの他の部分は、 サブシェルの履歴とカレントディレクトリの引き継ぎ、rsh コマンドに関するものです。色々な機能がありますが、これ全部で 25 行で済んでます。
最後に、プロンプトの設定 (starship.toml) です。RESTRICTED_SHELL=1 なら鍵の絵文字を出してるだけです。これの導入は必須ではないですが、あると便利です。
format = """ $time\ $cmd_duration\ $jobs\ $status\ $username\ $hostname\ $localip\ $shlvl\ $directory\ $git_branch\ $git_state\ $git_metrics\ $git_status\ $custom\ $sudo\ $shell\ $character""" [custom.restricted_shell] command = "echo 🔒" when = ''' test "$RESTRICTED_SHELL" = 1 '''
starship を使ってない人は、なんか上手いことやってください。
実際使ってみてどうか
まだ使い始めて 1 週間くらいですが、趣味でも業務でも苦なく使えています。普段使いできる保護レイヤーとしての役目は果たせてそうです。
ちなみに open . で Finder でディレクトリを開くのも、ちゃんと動きます。当初は Finder が Apple Seatbelt の制限を引き継ぐせいで上手く動かないのでは、と思っていたのですが、制限を引き継がずに起動されるようです。open で起動されたアプリケーションは、シェルではなく launchd が親プロセスになり、その結果 Apple Seatbelt による保護が効かないようです。保護を回避できて安全性が下がってしまってますが、普段使いする分にはこちらのほうが都合が良いとも言えます。
ちょっとしたトラブルもありました。VS Code をシェルから起動する code コマンドが上手く動作しないのです。具体的には、code . と打っても、カレントディレクトリを workspace として開いてくれません。

code コマンドの内部では open -n で VS Code を起動するのですが、その -n オプション (既にアプリケーションが起動していても新しいインスタンスを起動するオプション) の挙動が Apple Seatbelt の有無で変わるようです。
どうやら Apple Seatbelt では (allow default) していても、強制的に課される制限があるようです。
id:mizdra が確認した限りでは、ps コマンドや top コマンドの実行が制限されていました。
;; ~/.config/sandbox/allow-default.sb (version 1) (allow default)
$ sandbox-exec -f ~/.config/sandbox/allow-default.sb ps sandbox-exec: execvp() of 'ps' failed: Operation not permitted $ sandbox-exec -f ~/.config/sandbox/allow-default.sb top sandbox-exec: execvp() of 'top' failed: Operation not permitted
恐らく、他のプロセスの情報の取得が制限されているのだと思います。そのため open -n のようなコマンドが動かないのでしょう。
色々調べたところ、code コマンドを使わずに直接 open コマンドで VS Code を開けば、問題を回避できるようでした。僕は以下のような alias を貼るようにしてます。
if [[ "$RESTRICTED_SHELL" == "1" ]]; then alias code='open -a "Visual Studio Code"' fi
とはいえこれも完璧ではありません。code --wait のようなオプションが動作しなくなってしまいます *2。EDITOR="code --wait" を使ってる人は EDITOR="vim" に切り替えたりと、追加の対策を導入する必要があります。
code . が動かないのはちょっと面倒ですが、回避策もあるのでまあ…という感じですね。
面白いところ
ちょっと面白いのは、restricted-shell.sb を編集することで自由に保護レベルをカスタマイズできるところです。.zshrc や restricted-shell.sb の書き換えを禁止したり、open や docker の起動を禁止したり。好みに応じて変えられます。
やろうと思えばうんと厳しくもできます。Apple Seatbelt では (deny default) と書くと、デフォルトで全てが禁止され、allowlist のような形でルールを記述するモードになります。そうすれば、Sandbox のような厳しい制限も課せると思います。まあ普段使いするには厳しいでしょうが… それやるくらいなら、普通に Development Container 使ったら良いと思います。
macOS 以外への移植
Apple Seatbelt 相当のものがあれば、他の OS へも移植できるはずです。
僕は詳しくないですが、Linux だと AppArmor というものが使えるそうです。後日同僚の
id:Windymelt さんが Linux 版の記事を書いて下さるようなので、それを待って下さい。
それ以外の OS についても、興味がある人がいたら移植してみてください。
大事なこと
もちろん、restricted shell は完璧ではありません。restricted shell は (少なくとも今回紹介した sb ファイルでは) ファイルアクセスくらいしか制限してません。マルウェアからネットワークリクエストも、プロセスの起動も仕放題です。open コマンドを使えば保護を回避できます。マルウェアが本気を出せば、きっと色々なことができてしまうでしょう。Sandbox と呼ぶにはお粗末なものです。
セキュリティ対策に銀の弾丸はありません。様々な対策を幾重にも導入し、地道にリスクを下げていくべきです。例えば、以下のようなことをやると良いでしょう。
- npm package のインストール時に postinstall script を実行しない
- pnpm を使う or npm で
npm config set ignore-scripts trueする
- pnpm を使う or npm で
- pnpm で
minimumReleaseAgeを設定する - Socket Firewall を導入する
心構えや勉強も大事です。
- 信頼できる package をインストールするよう心がける
- 脆弱性や攻撃の情報に目を光らせ、発見されたらすぐに対応する
- セキュリティに関する書籍を読む
restricted shell はあくまで保護レイヤーの 1 つです。基本的なセキュリティ対策を疎かにしないよう気をつけましょう。
おわりに
この記事では「restricted shell」を紹介しました。本当にそのような緩い保護レイヤーで役に立つのか、疑問に感じる人も居るかもしれません。一方で、普段使いできる追加の保護レイヤーと考えると、面白い仕組みではないかと思ってます。
僕自身まだ1週間しか使ってないので、本当に使えるものなのか正直よく分かってません。しばらく使ってみて、使用感を確かめたいと思ってます。そのような感じですが、もし興味あれば使ったり、参考にしてみてください。
はてなエンジニア Advent Calendar 2025 2日目の担当は
id:pokutuna さんです。