セキュリティ:キャッシュ

守矢諏訪子 <suwako@076.moe>、2026-11-10 17:14
コミット 6287571d0d719969124961801b55a1f9804e14f5 ・親 62fe9d8 ・ファイルを見る

3個のファイル、+36 -6

@@ -263,7 +263,7 @@
263263 try {
264264 $handle = '';
265265 if (isset($params['handle'])) $handle = $params['handle'];
266- if ($handle === '') {
266+ if (!$this->isValidHandle($handle)) {
267267 header('Location: /openprovider/listcustomers');
268268 exit();
269269 }
@@ -287,7 +287,7 @@
287287 try {
288288 $handle = '';
289289 if (isset($params['handle'])) $handle = $params['handle'];
290- if ($handle === '') {
290+ if (!$this->isValidHandle($handle)) {
291291 header('Location: /openprovider/listcustomers');
292292 exit();
293293 }
@@ -342,7 +342,7 @@
342342 try {
343343 $tld = '';
344344 if (isset($params['tld'])) $tld = $params['tld'];
345- if ($tld === '') {
345+ if (!preg_match('/^[A-Za-z0-9-]{1,63}$/', $tld)) {
346346 header('Location: /');
347347 exit();
348348 }
@@ -447,7 +447,7 @@
447447 try {
448448 $domain = '';
449449 if (isset($params['domain'])) $domain = $params['domain'];
450- if ($domain === '') {
450+ if (!preg_match('/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z0-9-]{2,63}$/', $domain)) {
451451 header('Location: /');
452452 exit();
453453 }
@@ -465,5 +465,18 @@
465465 } catch (\Exception $e) {
466466 throw new \Exception($e->getMessage());
467467 }
468+ }
469+
470+ //////////
471+
472+ /**
473+ * 顧客ハンドルの形式を確認する(例:AB123456-XX)
474+ * URLの値はAPIのパスとキャッシュのファイル名にそのまま入るので、先に確認する
475+ *
476+ * @param string $handle 顧客ハンドル
477+ * @return bool 正しい形式ならtrue
478+ */
479+ private function isValidHandle(string $handle): bool {
480+ return preg_match('/^[A-Za-z0-9-]{1,64}$/', $handle) === 1;
468481 }
469482 }
@@ -58,6 +58,7 @@
5858 * @return Result
5959 */
6060 public function set(string $type, array $data): Result {
61+ if (!$this->isSafeName($type)) return Result::Error('エラー:不正なキャッシュ名。');
6162 if (!file_exists($this->dataDir)) {
6263 if (!mkdir($this->dataDir, 0755))
6364 return Result::Error('エラー:ユーザーのアイコンディレクトリの作成に失敗。');
@@ -77,6 +78,7 @@
7778 * @return array
7879 */
7980 public function get(string $type): array {
81+ if (!$this->isSafeName($type)) return [];
8082 if (!file_exists("{$this->dataDir}{$type}.json")) return [];
8183
8284 $content = file_get_contents("{$this->dataDir}{$type}.json");
@@ -95,6 +97,7 @@
9597 * @return Result
9698 */
9799 public function murder(string $name): Result {
100+ if (!$this->isSafeName($name)) return Result::Error('エラー:不正なキャッシュ名。');
98101 if (!file_exists("{$this->dataDir}{$name}.json")) return Result::Error('キャッシュファイルが存在しない。');
99102 if (!unlink("{$this->dataDir}{$name}.json")) return Result::Error('キャッシュファイルの削除に失敗。');
100103 return Result::Success();
@@ -114,5 +117,17 @@
114117 && (isset($cache[$dataPoint])
115118 && $cache[$dataPoint] === $data)
116119 && (isset($cache['lifespan']) && time() < ($cache['lifespan'] + $this->dataDuration)));
120+ }
121+
122+ ///////
123+
124+ /**
125+ * キャッシュ名はファイル名になるので、英数字・「.」「_」「-」だけを許可する(「..」は不可)
126+ *
127+ * @param string $name キャッシュ名
128+ * @return bool 安全ならtrue
129+ */
130+ private function isSafeName(string $name): bool {
131+ return preg_match('/^[A-Za-z0-9._-]{1,200}$/', $name) === 1 && !str_contains($name, '..');
117132 }
118133 }
@@ -78,9 +78,11 @@
7878 if (!OPENPROVIDER_ENABLED) return Result::Error('エラー:OpenProviderは無効です。');
7979 $cacheName = 'login';
8080 $cache = $this->cache->get($cacheName);
81- if (time() < ($this->lastAuth + $this->tokenDuration)) {
81+
82+ // 以前は$lastAuthが0のまま比較していたので、キャッシュのトークンが使われず毎回パスワードを送っていた
83+ $this->lastAuth = (int)($cache['last_auth'] ?? 0);
84+ if (isset($cache['data']['token']) && time() < ($this->lastAuth + $this->tokenDuration)) {
8285 $this->token = $cache['data']['token'];
83- $this->lastAuth = $cache['last_auth'];
8486 return Result::Success('', $cache);
8587 }
8688