github-table-attributes

このページは FormatArc のブログ記事が引用している実測の生データです。検索結果には表示されません (noindex)。

Files

FileSize
README.md4.0 KB
fixtures/01-colspan-rowspan.json114 B
fixtures/02-align.json93 B
fixtures/03-valign.json92 B
fixtures/04-width-percent.json98 B
fixtures/05-width-px.json95 B
fixtures/06-height.json91 B
fixtures/07-id.json90 B
fixtures/08-class.json96 B
fixtures/09-style.json96 B
fixtures/10-bgcolor.json94 B
fixtures/11-onclick.json99 B
fixtures/12-th-align.json115 B
fixtures/13-span-style-in-cell.json124 B
fixtures/14-br-in-cell.json87 B
measure.mjs3.5 KB
package.json283 B
results.json5.5 KB

README.md

# GitHub のテーブル内 HTML 属性サニタイズ実測

GFM のテーブルはセル結合を持たないため、結合したい場合は素の `<table>` HTML を書くことになる。そのとき **どの属性が GitHub を通り、どれが消えるか** を実測したもの。`gfm-table-cheatsheet` の「GitHub が許可する HTML 属性」節の一次ソースとして使う。

## なぜ測ったか

この表は 2026-05-13 の記事公開時から 4 locale に載っていたが、**出典が 1 本も無かった**。2026-08-05 の SEO Fix セッションで内容を検証したところ 2 箇所が実際に誤っていた。

| 属性 | 記事の旧記述 | 実測 |
|---|---|---|
| `width` / `height` | 「px 値は通るが `%` は除去されがち」 | **`width="50%"` はそのまま残る**。値による除去は起きない |
| `class` | 「無視される (GitHub のスタイルシートが固定なので効かない)」 | **除去される**。属性ごと消える |
| `style` | 「無視される」 | **除去される** |

「無視される」(属性は残るが適用されない) と「除去される」(属性ごと消える) は別の機構で、記事は同じ表の中で `<script>` / `on*` にだけ「除去」を使って書き分けていた。allowlist 方式では allowlist に無い属性はすべて消えるため、この書き分けは成立しない。

## 測定対象

GitHub 本番の Markdown レンダリングエンドポイント `POST https://api.github.com/markdown` (`mode: gfm`)。README / issue / PR 本文と同じパイプラインが返す HTML をそのまま読む。

`gjtorikian/html-pipeline` の `SanitizationFilter` allowlist を読む方法もあるが、それは「GitHub が使っているであろう実装」であって GitHub 本番の証明ではない。allowlist の読み取りだけを根拠に記事へ書くのは CLAUDE.md の数値根拠ルールで言う (3) 推定に当たるため、実レスポンスを一次データとして取っている。

## 成果物

| ファイル | 内容 |
|---|---|
| `fixtures/*.json` | 検証入力 12 件。`input` (Markdown)、`probeTag` (属性を見るタグ)、`id` を持つ |
| `measure.mjs` | 再現用スクリプト (`npm run measure`) |
| `results.json` | 入力・出力 HTML・属性ごとの判定 (`kept` / `removed` / `rewritten:<新しい値>`) |

## 実測結果

| 属性 | 判定 | 備考 |
|---|---|---|
| `colspan` / `rowspan` | 保持 | セル結合に使える組 |
| `align` (td / th) | 保持 | 列単位の配置を上書きできる |
| `valign` | 保持 | 記事の表には未掲載だった |
| `width` (`50%` / `120px`) | 保持 | **`%` でも px でも残る。値は検査されない** |
| `height` (`30`) | 保持 | 同上 |
| `id` | **書き換え** | `id="anchor-me"` → `id="user-content-anchor-me"`。属性は残るが接頭辞が付くため、書いたままの値ではアンカーリンクが張れない |
| `class` | 除去 | |
| `style` | 除去 | |
| `bgcolor` | 除去 | 記事の表には未掲載だった |
| `onclick` (`on*` ハンドラ) | 除去 | XSS 対策 |

`id` の書き換えは、記事が「アンカーリンクには使えない」と結論だけ書いていた部分の機構にあたる。理由は接頭辞の付与であって、属性が消えるからではない。

## 再現手順

```bash
gh auth status                                   # 認証済みであること
cd scripts/benchmarks/github-table-attributes
npm run measure                                  # results.json を上書き
```

依存パッケージは無い。`gh` CLI の認証だけを使う。

## 測定していないこと

Markdown API は 1 つのエンドポイントで、描画文脈 (README / issue / PR / コメント) を指定する引数を持たない。**文脈ごとに CSS が違う可能性は残るが、サニタイズ結果が文脈で変わることは本測定からは言えない**。記事でも文脈別の差には触れていない。

results.json

{
  "$schema": "./results.schema.json",
  "generatedAt": "2026-08-05T02:01:42.936Z",
  "measured": {
    "endpoint": "POST https://api.github.com/markdown (mode: gfm)",
    "via": "gh version 2.95.0 (2026-06-17)",
    "node": "v26.3.1",
    "note": "GitHub 本番のレンダリング結果。README / issue / PR 本文と同じパイプライン"
  },
  "notMeasured": {
    "scope": "README / issue / PR / comment の描画文脈ごとの差",
    "reason": "Markdown API は 1 つのエンドポイントで、描画文脈を指定する引数を持たない。文脈ごとに CSS が違う可能性は残るが、サニタイズ結果が文脈で変わることは本測定からは言えない"
  },
  "caseCount": 14,
  "results": [
    {
      "id": "colspan-rowspan",
      "input": "<table><tr><td colspan=\"2\" rowspan=\"3\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td colspan=\"2\" rowspan=\"3\">a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "colspan": "kept",
        "rowspan": "kept"
      }
    },
    {
      "id": "align",
      "input": "<table><tr><td align=\"center\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td align=\"center\">a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "align": "kept"
      }
    },
    {
      "id": "valign",
      "input": "<table><tr><td valign=\"top\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td valign=\"top\">a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "valign": "kept"
      }
    },
    {
      "id": "width-percent",
      "input": "<table><tr><td width=\"50%\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td width=\"50%\">a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "width": "kept"
      }
    },
    {
      "id": "width-px",
      "input": "<table><tr><td width=\"120px\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td width=\"120px\">a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "width": "kept"
      }
    },
    {
      "id": "height",
      "input": "<table><tr><td height=\"30\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td height=\"30\">a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "height": "kept"
      }
    },
    {
      "id": "id",
      "input": "<table><tr><td id=\"anchor-me\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td id=\"user-content-anchor-me\">a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "id": "rewritten:user-content-anchor-me"
      }
    },
    {
      "id": "class",
      "input": "<table><tr><td class=\"highlight\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td>a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "class": "removed"
      }
    },
    {
      "id": "style",
      "input": "<table><tr><td style=\"color:red\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td>a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "style": "removed"
      }
    },
    {
      "id": "bgcolor",
      "input": "<table><tr><td bgcolor=\"red\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td>a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "bgcolor": "removed"
      }
    },
    {
      "id": "onclick",
      "input": "<table><tr><td onclick=\"alert(1)\">a</td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td>a</td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "td",
      "verdict": {
        "onclick": "removed"
      }
    },
    {
      "id": "th-align",
      "input": "<table><tr><th align=\"right\" style=\"color:red\">a</th></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><th align=\"right\">a</th></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "th",
      "verdict": {
        "align": "kept",
        "style": "removed"
      }
    },
    {
      "id": "span-style-in-cell",
      "input": "<table><tr><td><span style=\"color:red\">a</span></td></tr></table>",
      "output": "<markdown-accessiblity-table><table role=\"table\"><tbody><tr><td><span>a</span></td></tr></tbody></table></markdown-accessiblity-table>",
      "probeTag": "span",
      "verdict": {
        "style": "removed"
      }
    },
    {
      "id": "br-in-cell",
      "input": "| a | b |\n| --- | --- |\n| x<br>y | z |",
      "output": "<markdown-accessiblity-table><table role=\"table\">\n<thead>\n<tr>\n<th>a</th>\n<th>b</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>x<br>y</td>\n<td>z</td>\n</tr>\n</tbody>\n</table></markdown-accessiblity-table>",
      "probeTag": "br",
      "verdict": {}
    }
  ]
}