- Daniel Mangumは、AT Protocolのblobアップロードとレコード参照構造を利用して、Bluesky PDSにアップロードした
text/htmlファイルを、ブラウザで開けるWebページのように提供できることを示した
- 中核となる動作は、アップロードされたblobがすぐには公開されず、何らかのrecordがblobを参照して初めてPDSが公開アクセス可能な状態に切り替える点にある
- 通常のBluesky画像embedは
image/* MIMEタイプのみ許可するが、未知のlexiconのrecordやopen unionベースのカスタムembedではtext/html blob参照を保存できる
getBlobには認証なしでアクセスでき、bsky.socialへのリクエストは実際のPDSであるporcini.us-east.host.bsky.networkへリダイレクトされてHTMLを返す
- Blueskyは
getBlobにCSPヘッダーとnosniffを適用し、デフォルトのblobアップロードを5MBに制限しているが、PDS運用サービスには任意ファイルのホスティングや保存・転送コストの負担が残る
AT Protocol blobでWebサイトを公開する方法
- この事例はBlueskyアプリの通常機能というより、AT ProtocolとPDS APIの動作仕様に基づくもの
- BlueskyはユーザーのPDSにアクセスするためのPDS entrywayを提供しており、複数のPDSインスタンスを
bsky.socialドメインで公開している
- 個々のPDSインスタンスにも直接アクセスでき、例のWebサイトは
porcini.us-east.host.bsky.networkのcom.atproto.sync.getBlobエンドポイントで開かれる
- 記事公開前にBlueskyチームと連絡を取り、この挙動は意図されたアプリケーションの使い方ではないものの既知の動作であり、脆弱性開示手続きには当たらないことを確認した
recordsとblobsが分離されている理由
- AT Protocolでは、アプリケーションデータは大きくrecordsとblobsに分かれる
- recordsはユーザーが作成する中核エンティティで、構造やメタデータを持てる
- blobsは画像のような、より大きく非構造なデータで、通常はrecordから参照されて初めて公開される
- Blueskyで画像付き投稿を作成すると、ユーザーには一度の操作に見えるが、APIレベルではblobアップロードとrecord作成が分離されている
- blob specificationによれば、blobはrecordから参照される前にまずPDSへアップロードされなければならない
- サーバーはアップロード時点ではそのblobがどのLexiconで使われるか分からないため、初期アップロード時には一般的なblob制限だけを適用し、record作成時点でLexiconベースの制限を適用する
- 正常にアップロードされたblobは一時ストレージに入り、この状態ではダウンロードや配布はできず、
listBlobsの出力にも含まれない
- record作成が成功し、そのrecordがblobを参照すると、サーバーはblobを公開アクセス可能な状態へ切り替える
HTML blobのアップロードと公開切り替え
- 認証トークンは
com.atproto.server.createSession XRPCメソッドでユーザー資格情報を交換して取得する
- 例では
com.atproto.sync.listBlobsで既存blob数を確認しており、アップロード前の数は391個だった
- Webサイト本体はシンプルな
index.htmlファイルで、com.atproto.repo.uploadBlobでアップロードされる
Content-Typeはtext/html
- 返されたblobメタデータにはCID
bafkreic5fmelmhqoqxfjz2siw5ey43ixwlzg5gvv2pkkz7o25ikepv4zeq、mimeType: text/html、size: 268が含まれる
- アップロード直後に
com.atproto.sync.getBlobで取得しようとしてもアクセスできず、listBlobsの件数も391のままだった
- Bluesky投稿の画像embedとしてHTML blobを参照すると、
app.bsky.embed.imageスキーマがimage/* MIMEタイプを要求するため失敗する
text/htmlのままだとWrong type of file. It is text/html but it must match image/*.というエラーが発生する
mimeTypeをimage/jpegに変えると、保存済みblobのMIMEタイプと参照MIMEタイプが異なるというエラーになる
未知のlexicon recordでblobを公開する
- blob型はBluesky専用ではなく、AT Protocolデータモデルの一部
- Bluesky PDS実装はオープンソースで、
findBlobRefsはLexValueを再帰的に探索して$type: blob参照を見つける
- PDSは時間の経過とともに新しいlexiconをサポートする必要があるため、自身が知らないlexiconも処理できなければならない
com.danielmangum.hack.website型のrecordを作成してHTML blobを参照させると、PDSはrecordを保存した
- 応答には
validationStatus: unknownが含まれる
- PDSは
com.danielmangum.hack.* lexiconを認識していないため検証できなかったが、recordの保存は許可された
- その後
listBlobsの件数は392に増え、blobが永続保存されたことが確認できた
getBlobは認証トークンなしで呼び出せる。recordとともにblobを扱う非認証主体もblobを取得できる必要があるためだ
bsky.socialのgetBlob呼び出しは302リダイレクトで実際のPDS URLを返し、-Lでリダイレクトを追うとHTML内容がそのまま返された
セキュリティヘッダー、CDN、直接アクセスの負担
- AT Protocol blob仕様のSecurity Considerationsでは、任意のユーザーアップロードファイルをWebサーバーから配信する際に生じるコンテンツセキュリティ上のリスクを扱っている
- 代表例は、同一originでスクリプトやSVGコンテンツが実行されるXSS問題
getBlobエンドポイントにはContent Security Policyの適用が事実上必須
- ブラウザやWebエージェントにblobストアを直接提供する方式は実質的にサポートされておらず、アプリケーションは独立したCDN、プロキシ、Webサービスなどを経由すべきとされる
- Bluesky PDSの
getBlobハンドラは推奨セキュリティヘッダーを適用している
x-content-type-options: nosniff
content-security-policy: default-src 'none'; sandbox
- デフォルトのblobアップロード上限は5MB
- Blueskyアプリケーションの一般的な画像blobはPDSから直接配信されず、
cdn.bsky.appのCDN URLとして提供される
- feed thumbnail URLとfull size URLが別々に存在する
- 投稿recordには画像CIDが含まれ、アプリケーション側がCDN提供方式を理解している必要がある
getBlobへの直接アクセスは、GitHub issue上で画像ラベリング、ユーザーコンテンツのエクスポート、将来のユースケースのために残されている
- 同じ議論には、ユーザーがコンテンツをhotlinkしたりBlueskyを無料ホスティングのように使ったりする可能性も含まれている
- 初期実装にはセキュリティヘッダーがなく、後から追加された
- 従来型のソーシャルプラットフォームでは有効なコンテンツ種類が限定されているため、アップロード時点でblobにより強い制限をかけられるが、BlueskyとAT Protocolの拡張性はより複雑な処理を要求する
open unionで投稿にカスタムembedを入れる
app.bsky.feed.post型は有効なembed一覧をunionで定義している
- AT Protocol lexiconではunionは明示的に
closedでない限り、デフォルトでopen
- 将来のスキーマ改訂で型が追加されうるため、実装は検証時に寛容であるべき
closedフラグがある場合は型集合が固定される
- Bluesky投稿のembed unionは
closedと記されていない
- そのため、列挙されていないembed
$typeでも投稿を作成できる
- 例では
app.bsky.feed.post record内にcom.danielmangum.hack.sites embedを入れ、その中でHTML blobを参照している
- 応答の
validationStatusはvalid
- Blueskyアプリケーションでは当該embedは静かに無視される
- recordにはコンテンツと参照が永続保存されるため、他のアプリケーションはこのembedをレンダリングできる
- このようなlexicon特性は、既存のユースケースの上に小さな拡張を付け足す形で活用でき、例として投稿内の小さなコード片をWebAssemblyサンドボックスで実行するような形も可能性として挙げられている
1件のコメント
Hacker News の意見
Daniel がチームに直接問い合わせてくれたことはありがたく、Blob ホスティングは悪用経路をさらに理解しながら調整を続けざるを得ない領域だと思う
今はこうした活用が実際に動いているのを見るのが面白いし、PDS は Web サーバーが Web サイトをホストするように、データベースホストになることを意図した構造になっている
以前いじって遊ぶにはよかったし、Bluesky に移ったとは知らなかったので、かなり興味深い
Bsky はすでに、ユーザードメインを redirect.bsky.com に ALIAS として紐づける方法をサポートしている: https://bsky.app/profile/jacob.gold/post/3kh6rnpdzmp2v
これがどのようなセキュリティコンテキストで実行されるのか気になって、
curlで PDS の Blob URL を確認してみたところ、レスポンスヘッダーにはaccess-control-allow-origin: *、content-security-policy: default-src 'none'; sandbox、x-content-type-options: nosniffなどがあったaccess-control-allow-origin: *は、どのドメインの Web ページの JavaScript からでもfetch()でこのコンテンツにアクセスできるという意味なので興味深く、default-src 'none'; sandboxは追加のスクリプトや画像を読み込めないようにし、JavaScript の実行も防ぐ非常に制限の強い設定なのでよい: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Co...ratelimit-limit: 3000は、おそらく IP ベースの制限に見える記事の末尾で WebAssembly の利用を提案しているのは正しい方向で、リンククリックの問題は、信頼できないコードを iframe に入れ、親側で
child-srcによって特定ドメインやdata:のみを許可する形で制限できるhttps://github.com/w3c/webappsec/issues/656#issuecomment-246...
https://www.w3.org/TR/CSP3/#exfiltration
sandboxを使う場合でもdefault-srcが必要なのか、それとも重複設定なのか気になるBluesky をBlob データストアとして使える可能性にはかなり期待している
友人と一緒に DOOM WAD を Bluesky に保存し、投稿のように「マップパック」を共有する方法を考えてみた。アカウント、リスト、スターターパックをフォローすれば、GZDoom のようなクライアントを改造して、それらのアカウントがアップロードした WAD を検索・閲覧できるようにできそうだ
Steam Workshop に似ているが、Bluesky を介する方式だ
Bluesky がBlob ストレージ用途のガイドラインに触れたことがあるのかも知らないし、DOOM ファイルは小さいのでよい例だが、大容量のサーバースペースを外部化するような形で悪用され得るのかも気になる
この特定の時代を経験したわけではないが、BlueSky ユーザーたちが技術的な深みへ飛び込んでいく様子は、MySpace で初めて HTML を学んでいた頃を思い出させる
ソーシャルメディア市場は以前よりはるかに飽和しているが、BlueSky から新世代のプログラマーが出てくるのか気になる
Bluesky は最初の段階だけを提供していて、その先はかなり大きな飛躍のように見える
HTTP/HTML なら、基本的な Python や他のプログラミング言語しか知らない人にも、14分以内にサーバーを一から作る方法を見せられる
テック大企業がコミュニティを搾取できないようにするには、ハッシュアドレス指定通信プロトコルが必要だと思うが、AT Protocol が HTTP のように革命を起こすほど目標をうまく捉えているかは確信が持てない
ここで扱われている PDS は Bluesky 製品そのものではなく、Personal Data Server の一部なので、結果として無限の無料データストレージのように見え得る点が重要なポイントの一つだ
Bluesky チームが時折言及しているサブスクリプションサービスは、こうした部分とつながる可能性があり、PDS でより多くの容量、帯域幅、高画質動画を提供するようなホスティングは、プレミアムティアとして売るのに適していそうだ
他の場所でもホストできるはずなので、Bluesky アプリを超えて相互作用できるという話は正しそうだ
インターネットに何かを投稿できるようにすると、結局誰かがその上で任意ファイルホスティングを試みるという現象には、名前が必要そうです
無制限だった時代には、ファイルシステムを Gmail などにバックアップしようとするオープンソースプロジェクトがいくつも登場し、Drive ができてからはさらにひどくなりました
無料サービスはこうした問題が起きることを見越して、基本的な制限を設けるべきです
https://en.wikipedia.org/wiki/Inner-platform_effect
こういうテーマが面白いなら、atfileも見る価値があります: https://github.com/electricduck/atfile
かなりよくできていて、記事の下部にリンクされている興味深い GitHub Issue をすぐ見られます
Bluesky の著名人 pfrazee も登場しています: https://github.com/bluesky-social/atproto/issues/523
AT には大きく期待しており、Fediverse で素晴らしい仕事をした賢い人たちも多いのでしょうが、このパラダイムのほうがより持続可能で現実的に見えます
基本は中央集権型で提供しつつ、必要なときや上級ユーザーには本当の分散化サポートを与える方式です
firehose ミラーリングから投資リターンを得る方法が必要で、たとえば一部ユーザーがより長い動画のホスティング費用を支払う Discord 代替、リレーがアクセスキーや復号鍵を管理して手数料を受け取る Patreon 的な構造、Bandcamp のようなソーシャルマーケットプレイスも可能に見えます
いずれにせよオープンなプラットフォームなので、今でもできない理由はありません
https://www.blockchaincapital.com/blog/bluesky-13m-users-and...
https://bsky.social/about/blog/09-11-2024-video
この方式はフィッシングやマルウェア配布に悪用されそうな気がしませんか
ホスティングサイトを作れば、誰かが悪意ある用途に使おうとする、というのはほとんどコンピューティングの法則のように見えます
Matrix の添付ファイルでも同じ問題がありました
*.bsky.networkが Google Safe Browsing、Palo Alto、Bluecoat などにブロックされ始めそうですこれは筆者の Bluesky サーバー、つまりPDS への直接リンクなので、当然筆者が制御しているものです
Strava の最近の API 変更を見て、彼らのプラットフォームに保存されたデータへのアクセスがどれほど制限されているかを思い出しました
フィットネス分野の支配的なサービスであるだけに、機能を段階的にサブスクリプションの壁の内側へ入れていくこともあり得ます
プライバシーや安全上の懸念はあるでしょうが、AT Protocol が GPX や FIT ファイルの保存プラットフォームとして適しているのか気になります
ただし AT Protocol はまだ非公開または限定公開の投稿をサポートしていないと認識しており、Strava 用途ではこの機能がかなり重要だと思います