【react/typescript】propsの渡し方をミスると大変なことに!?(気がついたことのまとめ)
みなさん、こんにちは。
てるし〜です。
今回、props
の渡し方をミスると大変なことになってしまうのでは?という記事です。
完璧な説明ができないとは思いますが、少しでも意識付けしていただければと記事にします。
props
の渡し方
私自身がやっている今回、簡単なソースを用意しました。
まず、AComponent
というコンポーネントを用意しました。
interface Props {
username: string;
}
export function AComponent(props: Props) {
return (
<>
<h1>AComponent</h1>
{props.username}
<br />
{JSON.stringify(props)}
</>
);
}
次に上記コンポーネントを呼び出すコンポーネントです。
import { AComponent } from "./components/a-component";
function App() {
const sample = {
username: "taro",
password: "hogehoge",
};
return <AComponent {...sample} />;
}
export default App;
上記コードは問題があります。
何が問題かわかるでしょうか?
上記ソースコードの問題
では、実行して描画された画面を見ていきましょう。
なんと、、、意図せずpassword
が渡ってしまっています。
「typescriptの型定義で良しなに防げている」とずっと勘違いしていました。
そして、大切なことを忘れていました。
typescriptはあくまでもAltJSの仲間であり実行上はJavascriptである
しかも、残念なことにReact上で上記コードでpassward
はスキームに定義されていないのに型エラーに引っかからずに通ってしまいました。
私はあまりセキュリティに詳しくはないのですが、Reactのprops
に渡ってしまっているということはDOMにパスワードの情報が載ってしまうことになりデベロッパーツール等でなんらかのもので閲覧されてしまい攻撃を受けてしまう危険性があるのではという考察が生まれました。
上記問題に対していくつかGPTちゃんと会話しながら解決方法を見出してみました。
あくまでも例ですが参考になればと
対策法1(ライブラリを使わない)
まずは、ライブラリを使わないバージョンで予期せず定義していないprops
を渡さない方法を見ていきます。
createSafeType
を作成する
import { createResult, type Result } from "./result";
function isStringArray(value: unknown): value is string[] {
if (!Array.isArray(value)) {
return false;
}
const findNotString: string | undefined = value.find(
(val) => typeof val !== "string"
);
if (findNotString === undefined) {
return true;
}
return false;
}
export function createSafeTypes<T extends object>(
raw: unknown,
allowedKeys: (keyof T)[]
): Result<Partial<T>, Error> {
if (typeof raw !== "object") {
return createResult.ng<Error>(new Error("オブジェクトじゃありません。"));
}
if (raw === null) {
return createResult.ng(new Error("nullはダメで〜〜す。"));
}
const rawKeys = Object.keys(raw);
for (const key of rawKeys) {
if (!isStringArray(allowedKeys)) {
return createResult.ng(
new Error("arrowKeysが文字列型の集団になっていません。")
);
}
if (!allowedKeys.includes(key)) {
return createResult.ng(new Error("存在しないpropsが定義されました。"));
}
}
return createResult.ok(raw);
}
簡単なものなのでざっくりとしたものになりますが、これで定義していないprops
がないかどうかを検証します。
定義したものだったらOKを、ダメならNGを返しています。
余談
Resultについては自作しています。
export const RESULT_OK = "ok" as const;
export const RESULT_NG = "ng" as const;
interface Success<T> {
readonly kind: typeof RESULT_OK;
value: T;
}
interface Failed<E> {
readonly kind: typeof RESULT_NG;
err: E;
}
export type Result<T, E> = Success<T> | Failed<E>;
export const createResult = {
ok: <T>(value: T): Success<T> => {
return {
kind: RESULT_OK,
value,
};
},
ng: <E>(err: E): Failed<E> => {
return {
kind: RESULT_NG,
err,
};
},
};
AComponent
を修正
import { createSafeTypes } from "../utils/create-safe-types";
import { RESULT_NG } from "../utils/result";
interface Props {
username: string;
}
const keys: (keyof Props)[] = ["username"];
export function AComponent(props: Props) {
const typeResult = createSafeTypes<Props>(props, keys);
//const zodResult = propsSchema.safeParse(props);
if (typeResult.kind === RESULT_NG) {
throw typeResult.err;
}
const mainProps = typeResult.value;
return (
<>
<h1>AComponent</h1>
{mainProps.username}
<br />
{JSON.stringify(props)}
</>
);
}
これで、実行すると。。。
このようにエラーで防ぐことができました。
App
のコンポーネントの呼び出しを変更
import { AComponent } from "./components/a-component";
function App() {
const sample = {
username: "taro",
password: "hogehoge",
};
return <AComponent username={sample.username} />;
}
export default App;
上記に変更することで、
必要な情報だけが描画されるようになりました!
対策法2(zodを使って)
次はzod
を使っていきます。
AComponent
の修正
import { z } from "zod";
const propsSchema = z
.object({
username: z.string(),
})
.strict();
type Props = z.infer<typeof propsSchema>;
export function AComponent(props: Props) {
const zodResult = propsSchema.safeParse(props);
if (zodResult.error) {
throw new Error("いらない型が入ってるよ");
}
const mainProps = zodResult.data;
return (
<>
<h1>AComponent</h1>
{mainProps.username}
<br />
{JSON.stringify(props)}
</>
);
}
これで実行してみると、
これもしっかりとガードできているようです。
import { AComponent } from "./components/a-component";
function App() {
const sample = {
username: "taro",
password: "hogehoge",
};
return <AComponent username={sample.username} />;
}
export default App;
に修正することで
上記のように必要な情報だけを抽出できます。
(6/16追記)対策3(eslintで未然に防ぐ)
ありがたいことにXでこんなのあるよとコメントしてくれたのでそれも記載します!
Thank you!!!!
今回はvite+react
でやってます。
パッケージインストール
pnpm i -D eslint-plugin-react
eslint.config.js
に追記
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
+import react from "eslint-plugin-react";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
+ react: react,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
+ "react/jsx-props-no-spreading": [
+ "error",
+ {
+ html: "enforce",
+ custom: "enforce",
+ exceptions: [],
+ },
+ ],
},
}
);
最終的には
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
react: react,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"react/jsx-props-no-spreading": [
"error",
{
html: "enforce",
custom: "enforce",
exceptions: [],
},
],
},
}
);
となります。
vscode上でみると、
おぉ〜〜〜〜!!!!エラーで弾いてる!!!
まとめ
今回は簡単にprops
を渡す上での注意についてをまとめました。
それぞれの対策方法はケースバイケースかなとは思っています。
他にもtypia
を使ってガードする方法、lodash
を使ってpick
で排除する方法などあるのではないかと思いますが私自身が検証できていないのでまたの機会にしようと思います。
Typescriptを使っているからと安心してはいけません!
後々悲しいことになります!
スプレッド構文で渡すのは確かにシンプルに書けるので良いですが、不要なものが入っていないかを確認、そして排除した上でprops
に渡しましょう!!!!
追記
なんか型でガードできるっぽいから今度やってみようかなと思いました。
Discussion