このページは FormatArc のブログ記事が引用している実測の生データです。検索結果には表示されません (noindex)。
| File | Size |
|---|---|
| README.md | 4.0 KB |
| fixtures/01-colspan-rowspan.json | 114 B |
| fixtures/02-align.json | 93 B |
| fixtures/03-valign.json | 92 B |
| fixtures/04-width-percent.json | 98 B |
| fixtures/05-width-px.json | 95 B |
| fixtures/06-height.json | 91 B |
| fixtures/07-id.json | 90 B |
| fixtures/08-class.json | 96 B |
| fixtures/09-style.json | 96 B |
| fixtures/10-bgcolor.json | 94 B |
| fixtures/11-onclick.json | 99 B |
| fixtures/12-th-align.json | 115 B |
| fixtures/13-span-style-in-cell.json | 124 B |
| fixtures/14-br-in-cell.json | 87 B |
| measure.mjs | 3.5 KB |
| package.json | 283 B |
| results.json | 5.5 KB |
# 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 が違う可能性は残るが、サニタイズ結果が文脈で変わることは本測定からは言えない**。記事でも文脈別の差には触れていない。
{
"$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": {}
}
]
}