From a323289278aa83150ddb3d4f0fde53dca72caa64 Mon Sep 17 00:00:00 2001 From: laiso Date: Fri, 1 May 2026 20:14:08 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20=E3=82=BB=E3=82=AD=E3=83=A5=E3=83=AA?= =?UTF-8?q?=E3=83=86=E3=82=A3=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E6=8C=87?= =?UTF-8?q?=E6=91=98=E3=81=AE=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 外部レビューで上がった妥当な 5 件をまとめて対応: - server: webhook トークン比較を crypto.timingSafeEqual に変更 (タイミング攻撃耐性) - server: quantity に整数 / 1-100 範囲のバリデーションを追加 - server: ALLOWED_ORIGINS 未設定時に警告ログを出力 - ios: Bundle ID typo "exmaple" を "example" に修正 (4 箇所) - README: Node.js 版数を CI 整合 (18 → 22)、CORS 既定の注意書きを追加 Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 4 ++-- .../project.pbxproj | 8 ++++---- server/index.js | 15 ++++++++++++++- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 097615c..21b351f 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ ## 前提条件 -- Node.js 18 以上 +- Node.js 22 以上 (CI 検証バージョン) - PAY.JP アカウント - PAY.JP ダッシュボードで Price オブジェクトを作成済み - PayPay を使うテスト環境 @@ -48,7 +48,7 @@ PAYJP_WEBHOOK_SECRET=your_webhook_secret - `PAYJP_SAMPLE_PRODUCT_NAME`: `/products` で返すサンプル商品名。未設定時は `テスト商品` - `PAYJP_SAMPLE_PRODUCT_AMOUNT`: `/products` で返すサンプル金額。未設定時は `100` -- `ALLOWED_ORIGINS`: CORS 許可オリジン。カンマ区切り。未設定時は全オリジン許可 +- `ALLOWED_ORIGINS`: CORS 許可オリジン。カンマ区切り。未設定時は全オリジン許可(**本番環境では必ず設定してください**) - `PORT`: サーバーポート。既定値は `3000` ### 3. サーバーを起動 diff --git a/ios/PayJPCheckoutExample.xcodeproj/project.pbxproj b/ios/PayJPCheckoutExample.xcodeproj/project.pbxproj index f5d1d3d..9ebcaf6 100644 --- a/ios/PayJPCheckoutExample.xcodeproj/project.pbxproj +++ b/ios/PayJPCheckoutExample.xcodeproj/project.pbxproj @@ -352,7 +352,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.exmaple.jp.pay.example-v2-checkout.PayJPCheckoutExample"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.jp.pay.example-v2-checkout.PayJPCheckoutExample"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -384,7 +384,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.exmaple.jp.pay.example-v2-checkout.PayJPCheckoutExample"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.jp.pay.example-v2-checkout.PayJPCheckoutExample"; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -405,7 +405,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.1; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.exmaple.jp.pay.example-v2-checkout.PayJPCheckoutExampleTests"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.jp.pay.example-v2-checkout.PayJPCheckoutExampleTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; @@ -426,7 +426,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.1; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = "com.exmaple.jp.pay.example-v2-checkout.PayJPCheckoutExampleTests"; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.jp.pay.example-v2-checkout.PayJPCheckoutExampleTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; diff --git a/server/index.js b/server/index.js index a547eed..3a58c9d 100644 --- a/server/index.js +++ b/server/index.js @@ -1,4 +1,5 @@ require('dotenv').config(); +const crypto = require('crypto'); const express = require('express'); const cors = require('cors'); @@ -7,6 +8,9 @@ const app = express(); const ALLOWED_ORIGINS = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) : undefined; +if (!ALLOWED_ORIGINS) { + console.warn('[security] ALLOWED_ORIGINS が未設定のため全オリジンを許可します。本番環境では必ず設定してください。'); +} app.use(cors(ALLOWED_ORIGINS ? { origin: ALLOWED_ORIGINS } : undefined)); app.use('/webhook', express.raw({ type: 'application/json' })); @@ -102,6 +106,10 @@ app.post('/create-checkout-session', async (req, res) => { return res.status(400).json({ error: '必須パラメータが不足しています' }); } + if (!Number.isInteger(quantity) || quantity < 1 || quantity > 100) { + return res.status(400).json({ error: 'quantity は 1〜100 の整数で指定してください' }); + } + if (!isAllowedRedirectUrl(success_url) || !isAllowedRedirectUrl(cancel_url)) { return res.status(400).json({ error: '許可されていないURLスキームです' }); } @@ -157,7 +165,12 @@ app.post('/webhook', (req, res) => { return res.status(500).json({ error: 'Webhook secret is not configured' }); } - if (webhookToken !== expectedToken) { + const tokenBuf = Buffer.from(String(webhookToken ?? ''), 'utf8'); + const expectedBuf = Buffer.from(expectedToken, 'utf8'); + if ( + tokenBuf.length !== expectedBuf.length || + !crypto.timingSafeEqual(tokenBuf, expectedBuf) + ) { console.error('Invalid webhook token'); return res.status(401).json({ error: 'Invalid webhook token' }); }