# スパム対策

> ハニーポット、署名付きタイムスタンプ、スパムスコア、人間確認、許可ドメイン、レート制限。

どのフォームにも、設定なしで多層の保護が付いています。ハニーポット、署名付きの描画タイムスタンプ、スパムスコア、レート制限です。さらに人間確認を追加したり、フォームを自分のドメインに限定したりできます。

| 保護レイヤー | デフォルトでオン | 結果 |
| --- | --- | --- |
| [レート制限](https://ikiform.com/ja/docs/rate-limits.md) | はい（標準プリセット） | `429 rate_limited` |
| [許可ドメイン](#allowed-domains) | いいえ | スパム、または `403 origin_not_allowed` |
| [人間確認](#human-check) | いいえ | `403 challenge_required` または確認ページ |
| [ハニーポット](#honeypot) | はい | スコアに +5 |
| [スパムスコア](#scoring) | はい | 5 以上でスパム |

## スパムの扱い

スパムと判定された送信はステータス `spam` で保存され、通常の成功の応答を返すので、ボットは何も学べません。スパムは Webhook もメールも送らず、月間の使用量にもカウントされず、30 日後に削除されます。ダッシュボードか [REST API](https://ikiform.com/ja/docs/api.md#mark-spam) で確認でき、スパムではないとマークすると、その時点で Webhook が送られます。

## ハニーポット

人が入力することのない非表示の入力欄を追加します。デフォルトの名前は `_gotcha` で、フォームの設定の **Honeypot field**（ハニーポットフィールド）でフォームごとに変更できます。隠し方は [HTML フォーム](https://ikiform.com/ja/docs/html.md#honeypot)を参照してください。

`ハニーポット`:

```html
<input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
```

## スパムスコア

送信ごとに、次のシグナルからスコアを計算します。5 以上でスパムとして保存されます。

| シグナル | ダッシュボードに表示される理由 | スコア |
| --- | --- | --- |
| ハニーポットに入力がある | `honeypot` | +5 |
| フォームの描画から 2 秒未満で送信された | `timestamp_fast` | +3 |
| 署名付きの描画タイムスタンプが偽造されている、または 24 時間より古い | `timestamp_invalid` | +3 |
| 名前や件名のフィールドにリンクがある | `url_in_name` | +3 |
| 同じフィールドが直近 24 時間以内にこのフォームに送られている | `duplicate` | +3 |
| リンクが合計 3 つ以上ある | `many_urls` | +2 |
| 既知のスパムの定型文（22 種類を内蔵） | `keyword` | +2 |
| 使い捨てメールのドメインのメールアドレス | `disposable_email` | +2 |

- 描画タイムスタンプが存在するのは、署名付きの `_ikiform_ts` を送る SDK か [shadcn/ui コンポーネント](https://ikiform.com/ja/docs/shadcn.md)でフォームを描画した場合だけです。これを含まないプレーンな HTML の送信は減点されません。
- 名前のフィールドとは、名前に `name`、`firstname`、`lastname`、`fullname`、`surname`、`subject` を含むフィールドのことで、`company`、`website`、`domain` のような語は含みません。
- スコアにかかわらず必ずスパムになる理由が 2 つあります。[許可ドメイン](#allowed-domains)による `origin_mismatch` と、Turnstile に接続できない場合の `captcha_unavailable` です。

## 人間確認

各フォームには、3 つの選択肢を持つ **Spam check**（スパムチェック）の設定があります。デフォルトはオフなので、フォームを作成しても、既存の `fetch` や curl の連携はそのまま動きます。

| 選択肢 | 内容 |
| --- | --- |
| オフ | チャレンジなし。ハニーポット、タイムスタンプ、スコアは引き続き動きます。 |
| Ikiform（推奨） | Ikiform が ikiform.com で動かす Cloudflare Turnstile。Cloudflare のアカウントやキーは不要です。 |
| 独自の Turnstile キー | 自分の Turnstile ウィジェットとシークレットキー。トークンのホスト名は許可ドメインと照合されます。 |

### Ikiform の確認の流れ

トークンの取得方法は、送信方法によって異なります:

- **HTML フォームの送信:** トークンのない送信には、ikiform.com 上の短い「人間であることを確認しています」ページが表示されます。このページが Turnstile を実行し、元のフィールドをトークン付きで再送信したあと、ブラウザーは通常どおりリダイレクト先に移動します。
- **JavaScript:** SDK の `challenge(formId)` を呼び出します。小さな ikiform.com のフレームをマウントし、Turnstile がクリックを必要としない限り表示されず、トークンで resolve されます。それを `turnstileToken` として `submit` に渡します。トークンのない `fetch` の送信には `403 challenge_required` が返ります。
- **shadcn/ui コンポーネント:** 自動で処理されます。
- **シークレットキーを使うサーバー:** サーバーは確認を解けないため、スキップされます。

`form.js`:

```js
import { challenge, submit } from "@ikiform/sdk";

const form = document.querySelector("form");

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const turnstileToken = await challenge("frm_xxxxxxxxxxxxxxxx");
  const result = await submit("frm_xxxxxxxxxxxxxxxx", new FormData(form), { turnstileToken });
  if (result.ok) form.replaceWith("Thanks, we got it.");
});
```

トークンは 1 回限りで有効期間は 300 秒なので、送信ごとに 1 つ取得してください。トークンのない JSON の送信には `challenge_url` 付きの `403 challenge_required` が、検証に失敗したトークンには `403 captcha_failed` が返ります。

`403 Forbidden`:

```json
{
  "ok": false,
  "error": {
    "code": "challenge_required",
    "message": "Complete the human check, then send the form again.",
    "challenge_url": "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx/challenge"
  },
  "request_id": "req_01k7v2c9t3e8m5q4x6z0a1b2c3"
}
```

> Turnstile 自体に接続できない場合、送信は受け付けられ、理由 `captcha_unavailable` でスパムとして保存されるので、失うことなく元に戻せます。

### 独自の Turnstile キー

サイトキーとシークレットキーをフォームの設定に貼り付けます。シークレットは保存時に暗号化されます。フォームにウィジェットを描画して送信に `cf-turnstile-response` を追加させるか、トークンを `turnstileToken` として `submit` に渡します。有効なトークンのない送信には、シークレット API キーを使った送信も含め、`403 captcha_failed` が返ります。

## 許可ドメイン

フォームを置くドメインを **Allowed domains**（許可ドメイン）に登録します。すると送信は、`Origin` ヘッダー（オリジンがない場合は `Referer`）で判定して、それらのドメインかそのサブドメインから送られたものでなければならなくなります。どのサイトからの送信も受け付けるには、一覧を空にしておきます。

| 許可ドメイン | 送信元 | 一致するか |
| --- | --- | --- |
| `example.com` | `https://example.com` | はい |
| `example.com` | `https://www.example.com` | はい（サブドメイン） |
| `example.com` | `http://localhost:3000` | いいえ。開発中は `localhost` を追加 |
| `example.com` | `https://example.co` | いいえ |
| `app.example.com` | `https://example.com` | いいえ。親ドメインは一致しない |

**When a post comes from another site**（他のサイトから送信された場合）の設定で、一致しないときの動作を決めます。**Mark as spam**（デフォルト）か、`403 origin_not_allowed` で **Reject** するかです。シークレット API キーを使った送信はこのチェックをスキップします。許可ドメインは、どの `_redirect` 先に従うか、Ikiform の確認フレームをどこに埋め込めるかも決めます。

## レート制限

各フォームには **Rate limit** のプリセットがあり、訪問者の IP とフォームごとに正確にカウントされます。標準は 1 分に 5 件、緩めは 1 分に 20 件、厳しめは 10 分ごとに 2 件です。詳しいルールは[レート制限](https://ikiform.com/ja/docs/rate-limits.md)にあります。
