セキュリティ
Murasaki の信頼境界、組み込み runtime protection、アプリ側の責務。
デスクトップセキュリティはプロセス境界から始まります。Murasaki は Node を renderer から分離し、アプリ WebView の遷移先を自身の origin に制限し、特権 loopback endpoint を app-session token で保護します。通常の renderer bug の影響を減らしますが、完全な 権限システムではありません。
renderer native command は deny-by-default で、型付き capabilities list から許可
します。宣言windowごとに独立したallowlistがありますが、argument / path / URL scope
はありません。renderer-to-Node関数もprivileged application codeとして扱い、surfaceを
小さく保ってください。
信頼境界
| 境界 | 組み込み保護 | アプリ側の責務 |
|---|---|---|
| Remote URL → app WebView | app origin と完全一致する遷移だけを window 内で許可。off-origin HTTP(S) は system browser で開き、file: / data: / javascript: は拒否 | navigation behavior を弱めず、remote script を renderer に注入しない |
| Renderer → Rust native command | exact-origin IPC checkと、call元windowごとに評価するtyped / deny-by-default allowlist | そのrendererが使うcommandだけをgrant。window:manageなど広い操作のapp-level intentを検証 |
| Renderer → local Node | /api/* と /__murasaki/* は random 256-bit HttpOnly / SameSite=Strict cookie を要求し Host / Origin を検証。wire payload に上限 | 各 action / route / Main function 内で引数を検証し操作を認可 |
| OS URL / file activation → Node Main | 設定済み scheme / extension だけを normalize して openRequested() target にし、認証付き app-local channel で配送 | URL / path をすべて attacker-controlled として扱い、操作前に intent、identifier、host、file content を検証 |
| App → update host | manifest raw bytes の Ed25519 signature、その後 payload の SHA-256 を検証 | private update key を保護し、hash/signature を同時に公開 |
| OS → installed app | macOS Developer ID / notarization と Windows Authenticode orchestration が最終 app-owned artifact を署名・検証 | platform certificate / provider を用意して保護し、clean machine で trust policy と installed artifact をテスト |
本番は全 interface ではなく 127.0.0.1 だけで listen します。開発時は loopback の
localhost / 127.0.0.1 / [::1] を許可し、cross-site Fetch Metadata を拒否します。
session cookie は最初の document response が設定し、JavaScript からは読めません。
Native command を明示的に許可する
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
window: {
route: '/',
capabilities: ['app:quit', 'dialog:openFile', 'shell:openExternal', 'window:open'],
},
windows: {
preview: { route: '/preview', capabilities: ['clipboard:writeText'] },
settings: { route: '/settings', capabilities: [] },
},
})capabilityを省略すると何も許可されません。ただしprimaryはwindow.capabilitiesがない
場合だけlegacy top-level listへfallbackし、secondaryは継承しません。未知の文字列も何も
許可せず、bridgeはtrusted app originと完全一致するcallだけを受け付けます。allowlistは
command単位で、dialogを1 directory、external URLを1 hostに限定できません。
window:manageは全宣言windowをtargetにできるため、信頼するrendererだけへ付与し、より
狭いintentはapp codeでも強制します。application全体のprogrammatic shutdownは
app:quit で別に制御されます。
シークレットは Node に置く
renderer code から import したものは public client JavaScript になり得ます。API token、
private key、DB credential、license validation は src/main.ts、'use main'、
'use server'、server-only API route module に置きます。
'use main'
export async function loadAccount(accountId: string) {
if (!/^[a-z0-9_-]{1,64}$/i.test(accountId)) {
throw new TypeError('invalid account id')
}
const token = process.env.ACCOUNT_API_TOKEN
if (!token) throw new Error('account service is not configured')
const response = await fetch(`https://api.example.com/accounts/${accountId}`, {
headers: { authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(10_000),
})
if (!response.ok) throw new Error(`account service returned ${response.status}`)
return response.json()
}runtime session は request がアプリ renderer origin を経由したことを示しますが、 renderer が侵害されていないことは保証しません。XSS は client bundle から呼べる 関数を実行できます。
Path と URL を検証する
renderer input を無制限の filesystem path に連結しないでください。app-owned directory 配下に resolve し、結果がその内側に留まることを確認します。raw path より opaque ID を優先してください。
Node 関数が fetch() / URL open を行う前には protocol と host を allowlist します。
これは意図しない local-network access と SSRF を防ぎます。renderer の request header
をそのまま remote service に転送しないでください。
同じ rule は openRequested() にも適用されます。登録済み scheme は dispatch の仕組みであり、
認証の仕組みではありません。local process、browser、document のいずれからも open を
試行できます。file association も path を示すだけで、trusted file を示すものではありません。
import { defineMain } from 'murasaki/main'
export default defineMain({
async openRequested(_context, event) {
for (const target of event.targets) {
if (target.kind === 'url') {
const url = new URL(target.url)
if (url.protocol !== 'example-notes:' || url.hostname !== 'open') continue
const id = url.pathname.slice(1)
if (!/^[a-z0-9_-]{1,64}$/i.test(id)) continue
// 検証済み identifier を application code で解決する。
} else {
// target.path を parse する前に extension、access policy、size、
// file format を確認し、名前だけを根拠に content を実行しない。
}
}
},
})deep-link URL へ renderer を自動 navigation させたり、query 値を HTML に埋め込んだり、 登録済み extension があるという理由で file を実行したりしないでください。renderer に 結果が必要なら、Main event API から狭く検証済みの値だけを渡します。
Content Security Policy
Murasaki は document に X-Content-Type-Options: nosniff と Referrer-Policy: no-referrerを設定し、framework-owned / user-owned HTMLへCSP meta tagを1つ注入します。
production既定値はscript-src 'self'を使い、object、frame、base URL変更、inline script
attribute、他originへのform送信を遮断します。一方remote-backed appのHTTPS / WSS接続は
許可し、image / font / mediaには一般的なHTTPS / data / blob sourceを許可します。
Reactのstyle attributeとruntime CSS互換性のため、style-src 'self' 'unsafe-inline'は
残しています。
developmentでは別policyを使い、Vite / React Refreshが注入するinline scriptとHMR用の
ws:を追加で許可します。'unsafe-eval'は追加せず、この緩和はproductionには入りません。
policy全体のoverride、または別layerが管理する場合のopt-outを設定できます。
export default defineConfig({
appId: 'com.example.notes',
productName: 'Notes',
security: {
csp: "default-src 'self'; script-src 'self'; style-src 'self'; connect-src 'self' https://api.example.com; object-src 'none'; base-uri 'none'; frame-src 'none'",
},
})
// Murasakiの注入だけを無効化:
// security: { csp: false }stringはdirective mergeではなく完全なoverrideです。user-owned index.htmlにCSP metaが
あればそれを維持して<head>先頭へ移動します。既存tagとsecurity.csp stringの両方を
設定すると、片方を暗黙に選ばずbuild errorにします。blockされたsourceはbrowser consoleの
CSP violationで確認し、そのsourceだけを追加するかapp originへ移してください。
既存appを移行するときはproductionのinline scriptをexternal moduleへ移します。remote script / frameは既定でblockされるため、必要なら明示的にoverrideしてください。HTTPS / WSS backendは既定で利用できます。remoteのplain HTTP backendにはoverrideが必要です (same-origin loopback callは許可されます)。
meta配信CSPにはbrowser定義の制約があります。tagをparseした位置から適用されるため
Murasakiは<head>先頭へ注入または移動します。frame-ancestors、sandbox、reportingなどの
directiveはmeta tagから完全には利用できないため、既定policyにも含めていません。これらが
必要な実network serviceではresponse headerを使ってください。CSPはHTMLをsanitizeせず、
Node関数をauthorizeしません。
既定のinline style許可も互換性上のtradeoffであり、XSS安全性の保証ではありません。
引き続きdangerouslySetInnerHTMLを避け、user-authored HTMLをsanitizeし、third-party codeを
固定してください。
実ネットワークサービスを公開する場合
Murasaki API route は app-local で、public service としては到達できません。Main から Node の TCP / WebSocket / HTTP listener を作れますが、Murasaki runtime-session 保護の 外側に新しい security boundary ができます。
- remote client が明示的な製品機能でなければ
127.0.0.1に bind - message 処理前に認証
- message / payload 上限を定義
- 高コスト処理を rate limit
shutdown()で listener を閉じる- Murasaki runtime token を独自 protocol の credential に再利用しない
Code signing と update key は別物
Code signing は OS に「誰がこの executable を作ったか」を示します。Ed25519 update
signature は Murasaki に「この manifest を app の update key 保有者が承認したか」を
示します。本番 updater は両方を使ってください。OS artifact は Developer ID +
notarization または Murasaki の Windows Authenticode --sign flow、更新は manifest signature
と payload hash で保護します。
既知の security gap
- Native capabilityはcommand単位・window別だが、path / URL / argument scopeや独立policy fileがない
- 宣言windowは1つのapplication originと認証済みlocal Node sessionを共有する。native
allowlistはServer Actions、
'use main'、API route、updater endpointを隔離しないため、 application code側で操作を認証・認可する必要がある - Linux package signing は未実装。Windows Authenticode には developer が用意した certificate / Artifact Signing provider と Windows SDK SignTool が必要
- Linux の protocol / file-association 登録は未実装。Windows portable archive は意図的に self-register しない
- renderer native APIは
src/main.tsから直接呼べない
正確な状況はプラットフォームと機能の状況 で確認できます。
脆弱性を報告する
public issue を作らないでください。private GitHub Security
Advisory または
[email protected] を利用してください。1.0 未満では最新 minor release が
サポート対象です。