ColdFusionでワンタイムトークン実装

自分用メモ。

こちらを参考にさせて頂きました。
[実装例に見る、ワンタイムトークンでWebサイトをリプレイアタックから守る方法](https://www.webprofessional.jp/how-to-prevent-replay-attacks-on-your-website/)

```html:sample.cfm
<!--- ワンタイムトークンチェック --->
<CFIF cgi.REQUEST_METHOD eq "POST">
	<CFSET tokenId = getToken(form.hiddenToken, 1, "-")>
	<CFSET tokenStr = getToken(form.hiddenToken, 2, "-")>
	<CFLOCK timeout="3" scope="session">
		<CFIF isDefined("session.hiddenToken") && structKeyExists(session.hiddenToken, tokenId) && session.hiddenToken[tokenId] eq tokenStr>
			<!--- トークン無効化 --->
			<CFSET structDelete(session.hiddenToken, tokenId)>
		<CFELSE>
			<!--- エラー処理 --->
			<CFTHROW message="不正なページ遷移です。">
		</CFIF>
	</CFLOCK>
</CFIF>

<!--- ワンタイムトークン生成 --->
<CFLOCK timeout="3" scope="session">
	<CFIF isDefined("session.tokenId") && isNumeric(session.tokenId) && session.tokenId lt 100>
		<CFSET session.tokenId += 1>
	<CFELSE>
		<CFSET session.tokenId = 1>
	</CFIF>
	<CFSET session.hiddenToken[session.tokenId] = makeRandomString(20)>
	<CFSET tokenId = session.tokenId>
	<CFSET tokenStr = session.hiddenToken[session.tokenId]>
</CFLOCK>

<!--- 画面出力 --->
<CFOUTPUT>
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>ワンタイムトークン実装サンプル</title>
</head>
<body>
	<form id="form1" method="post">
		<div>
			<input type="submit" name="btnSubmit" value="Submit">
			<input type="hidden" name="hiddenToken" value="#tokenId#-#tokenStr#">
		</div>
	</form>
</body>
</html>
</CFOUTPUT>

<!--- 擬似乱数生成関数 --->
<cfscript>
	function makeRandomString(length){
		var aChars = ListToArray("0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z");
		var sReturnString = "";
		for(i=1; i<=length; i++){
			sReturnString &= aChars[RandRange(1, arrayLen(aChars))];
		}
		return sReturnString;
	}
</cfscript>
```

トークンIDは100番までをサイクリックに使用していますので適宜変更してください。
擬似乱数の桁数もお好みで。

以下はセッション変数ではなく、クライアント変数を使うバージョンです。クライアント変数の保管先がCookieの場合、ユーザーが書き換え可能であるため、リプレイアタック対策にはなりません。多重POST対策の用途にどうぞ。

```html:sample2.cfm
<!--- ワンタイムトークンチェック --->
<CFIF cgi.REQUEST_METHOD eq "POST">
	<CFSET tokenId = getToken(form.hiddenToken, 1, "-")>
	<CFSET tokenStr = getToken(form.hiddenToken, 2, "-")>
	<CFIF structKeyExists(client, "hiddenToken_#tokenId#") && client["hiddenToken_#tokenId#"] eq tokenStr>
		<!--- トークン無効化 --->
		<CFSET structDelete(client, "hiddenToken_#tokenId#")>
	<CFELSE>
		<!--- エラー処理 --->
		<CFTHROW message="不正なページ遷移です。">
	</CFIF>
</CFIF>

<!--- ワンタイムトークン生成 --->
<CFIF isDefined("client.tokenId") && isNumeric(client.tokenId) && client.tokenId lt 100>
	<CFSET client.tokenId += 1>
<CFELSE>
	<CFSET client.tokenId = 1>
</CFIF>
<CFSET tokenStr = makeRandomString(20)>
<CFSET "client.hiddenToken_#client.tokenId#" = tokenStr>

<!--- 画面出力 --->
<CFOUTPUT>
<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>ワンタイムトークン実装サンプル（クライアント変数版）</title>
</head>
<body>
	<form id="form1" method="post">
		<div>
			<input type="submit" name="btnSubmit" value="Submit">
			<input type="hidden" name="hiddenToken" value="#client.tokenId#-#tokenStr#">
		</div>
	</form>
</body>
</html>
</CFOUTPUT>

<!--- 擬似乱数生成関数 --->
<cfscript>
	function makeRandomString(length){
		var aChars = ListToArray("0,1,2,3,4,5,6,7,8,9,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z");
		var sReturnString = "";
		for(i=1; i<=length; i++){
			sReturnString &= aChars[RandRange(1, arrayLen(aChars))];
		}
		return sReturnString;
	}
</cfscript>
```