うならぼ

申し訳程度のアフィリエイトとか広告とか解析とかは/aboutを参照

CSPを避けてUserScriptでCSS追加するやつ

拝啓 UserScript作者の皆様におかれましてはいかがお過ごしでしょうか。 わたしは以前書いたUserScriptが、サイト側のCSP導入によって <style> の挿入だけ動かなくなっていました。

今日はその話。

いつものやつ

document.head.insertAdjacentHTML('beforeend', `
<style>
  #foo { display: none }
  #bar { cursor: pointer }
</style>
`);

…このシンタックスハイライト、Template literal 認識してなくない?

CSPでunsafe-inlineが許可されなくなると、こんな感じで蹴られるようになる。

Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self' https:". Either the 'unsafe-inline' keyword, a hash ('sha256-alRSRYviRMwuItyDEgtLXyYEpCVEYkqt5i6LJx1Oes4='), or a nonce ('nonce-...') is required to enable inline execution.

この時 HTMLStyleElement#sheetnull になっていることで判別ができる(普通は appendChild したところで CSSStyleSheet が入る)。

別解1. Constructable Stylesheet Objects を使う

https://wicg.github.io/construct-stylesheets/

Motivationを斜め読みした感じ…Web Components で Shadow Root の中にだけ適用されるスタイルシートが増えるなかで、スタイルシートの処理や内部表現を効率化したい。styleタグについてはそういうことをしたりもするけど、JS側でスタイルを変更することもあってどうしたものか。というところで、CSSStyleSheetをJS側で作って再利用できるようにしよう、ということらしい?

で現状Blinkでしか使えないこのインターフェイス、なんとCSPの制約を受けない。将来もそうなのかはどうなんだろうなと思いつつ。

const sheet = new CSSStyleSheet();
// syncでない、Promise返す版もある
sheet.replaceSync(`
  #foo { display: none }
  #bar { cursor: pointer }
`);
// adoptedStyleSheets は FrozenArray なので要素の追加はできない。再代入はできる。
document.adoptedStyleSheets = document.adoptedStyleSheets.concat(sheet);

別解2. 既存の CSSStyleSheet にルールを追加する

CSPが効いていようと、既にドキュメントに関連付けられている CSSStyleSheet には(同一オリジンなら)ルールを追加できる。

// 同一オリジンで最も後ろにあるスタイルシートを探す
const usableSheet = [...document.styleSheets].filter(x => x.href?.startsWith(location.origin)).slice(-1)[0];
usableSheet.insertRule('#foo { display: none }`, usableSheet.cssRules.length);
usableSheet.insertRule('#bar { cursor: pointer }`, usableSheet.cssRules.length);

@media などのat-rulesも書ける。惜しいのは複数のルールをまとめて追加できないことだけども、条件が常に満たされるようなグループに入れてしまえば大抵いけそうではある。

const usableSheet = [...document.styleSheets].filter(x => x.href?.startsWith(location.origin)).slice(-1)[0];
usableSheet.insertRule(`@supports (display:block) {
  #foo { display: none }
  #bar { cursor: pointer }
}`, usableSheet.cssRules.length);

あとがき

最終的にきっかけとなったUserScriptは <style> → adoptedStyleSheets → insertRule の順に試すようにした。ただこれだと一度style要素を挿入してみる際にコンソールにエラーが出力されてしまうので、邪魔と言えば邪魔。サイレントに確認する方法を探るか、あるいはその方法はスキップしてしまってもいいかもしれない。