📌

Input要素のmaxlength属性は信用してはいけない

2024/02/11に公開

はじめに

input 要素の maxlength 属性は比較的よく利用される属性です。

https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/maxlength

しかし、maxlength 属性信用して、バリデーションを省略すると、特殊な方法(ブラウザの開発者ツールで maxlength 属性を削除するなど)により、
不正なデータが送信されてしまうリスクがあることは周知の事実です。

このような操作は一般のユーザには難しく、また意図的に操作をしないと発生しないためエッジケースだと考えていましたが、
一般ユーザでも容易に maxlength 以上の文字列が入力できるケースに遭遇したため、対策を検討したいと思います。
(今回は React ですが、原理は素の js や Vue などの他のライブラリにも適用できると思います。)

再現方法

再現環境

macOS x Safari x macOS 標準 IME
(Google 日本語入力を利用していると再現しません)

再現手順

  1. maxlength 属性が設定された input 要素に、日本語入力で maxlength 以上の文字を入力する (まだ確定はしない)
  2. どこでもいいので、input 要素の外側をクリックする
  3. 入力していた文字が(maxlength を超過していたとしても)全て imput 要素に反映される

※ 他の環境では、3.の時点で超過分は除去されて、input 要素に反映されます

対策

まず、input 属性をラップしたコンポーネントを作成します。

Input.tsx
import React, { useCallback, forwardRef } from "react";

// -----

type Props = React.ComponentPropsWithoutRef<"input">;

export const Input = forwardRef<HTMLInputElement, Props>(
  (
    { ...props }: Props,
    ref: React.Ref<HTMLInputElement>
  ): React.ReactElement => {

    return (
      <input
        ref={ref}
        {...props}
      />
    );
  }
);

// -----

続いて、IME 確定イベントの handler を定義します。

Input.tsx
export const Input = forwardRef<HTMLInputElement, Props>(
  (
-     { ...props }: Props,
+     { onCompositionEnd, ...props }: Props,
    ref: React.Ref<HTMLInputElement>
  ): React.ReactElement => {

+     // IME確定イベント
+     const handleCompositionEnd = useCallback(
+       (event: React.CompositionEvent<HTMLInputElement>) => {
+         console.log("handleCompositionEnd", event);
+
+         onCompositionEnd?.(event);
+       },
+       []
+     );
+
+     // -----

    return (
      <input
        ref={ref}
        {...props}
+         onCompositionEnd={handleCompositionEnd}
      />
    );
  }
);

// -----

IME 確定時に、 maxLength を超過している場合は value を補正する処理を追加します。

Input.tsx
export const Input = forwardRef<HTMLInputElement, Props>(
  (
-     { onCompositionEnd, ...props }: Props,
+     { maxLength, onCompositionEnd, ...props }: Props,
    ref: React.Ref<HTMLInputElement>
  ): React.ReactElement => {

    // IME確定イベント
    const handleCompositionEnd = useCallback(
      (event: React.CompositionEvent<HTMLInputElement>) => {
        console.log("handleCompositionEnd", event);

+         if (
+           maxLength !== undefined &&
+           event.target instanceof HTMLInputElement &&
+           event.target.value.length > maxLength
+         ) {
+           console.log("handleCompositionEnd", "fix value");
+
+           const input = event.target;
+
+           // -----
+
+           const newValue = input.value.slice(0, maxLength);
+
+           // -----
+
+           // input イベント発火
+           // https://stackoverflow.com/a/46012210
+           Object.getOwnPropertyDescriptor(
+             window.HTMLInputElement.prototype,
+             "value"
+           )?.set?.call(input, newValue);
+           input.dispatchEvent(
+             new Event("input", { bubbles: true, cancelable: true })
+           );
+         }
+
        onCompositionEnd?.(event);
      },
      []
    );

    // -----

    return (
      <input
        ref={ref}
        {...props}
+         maxLength={maxLength}
        onCompositionEnd={handleCompositionEnd}
      />
    );
  }
);

一度挙動を見てみます。

いい感じですが、まだ考慮不足のケースがありそうです。
(以下のケースでは、超過後に入力された「さしす」が消えてほしい)

value を切り取る位置の起点を、末尾から範囲選択の終端に変更します。

https://developer.mozilla.org/ja/docs/Web/API/HTMLInputElement#:~:text=されます。-,selectionEnd,-unsigned long%3A 選択

Input.tsx
          // -----

-           const newValue = input.value.slice(0, maxLength);
+           const newValue = `${input.value.slice(
+             0,
+             maxLength - (input.value.length - (input.selectionEnd ?? 0))
+           )}${input.value.slice(input.selectionEnd ?? 0)}`;

          // -----


これで Chrome や Google 日本語入力の挙動と揃いました。

9 文字(あいうえおかきくけこ)の途中に 3 文字(さしす)を追加して、2 文字(しす)が削除される挙動も問題なさそうです。

できたもの

実際の挙動は以下からお試しいただけます。

https://codesandbox.io/p/sandbox/input-maxlenght-react-9l3wt3

さいごに

この対策を行ったところで、別の抜け道はあるかもしれないので、データ送信前のバリデーションや、
サーバーサイドでのバリデーションは行うようにしましょう。
また、この事象は macOS の IME や Safari の一時的な不具合の可能性がありますので、
そのうち解消されるのではないかと考えています。

よりよい方法や考慮漏れのケースがあればコメント欄などでお知らせいただけると助かります。

GitHubで編集を提案
株式会社ナンバーフォー

Discussion