Skip to content

[] Add getAppMeta/getAppLicenses to lib/api.ts, create app/about/page.tsx and app/licenses/page.tsx (with tests)#298

Merged
zoetaka38 merged 2 commits into
mainfrom
feature/019f0d7d8aeb-019f0d7d8aeb
Jun 29, 2026
Merged

[] Add getAppMeta/getAppLicenses to lib/api.ts, create app/about/page.tsx and app/licenses/page.tsx (with tests)#298
zoetaka38 merged 2 commits into
mainfrom
feature/019f0d7d8aeb-019f0d7d8aeb

Conversation

@red-codens

@red-codens red-codens Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Type of Change

  • feat — new feature
  • fix — bug fix
  • chore — tooling / maintenance
  • docs — documentation only
  • refactor — code change that neither fixes a bug nor adds a feature
  • test — adding or updating tests
  • ci — CI/CD configuration

Testing

Checklist

  • Tests pass locally
  • Lint clean (task lint)
  • Commit messages follow Conventional Commits

Task Goal

Target Repository

Corevice/open-git
All work for this task belongs in this repository. Do the implementation inside this repository's directory in the workspace; do not search or edit other repositories unless this task explicitly requires a cross-repo change.

Context

The /about and /licenses pages are public-facing pages required by the PRD to display product metadata and third-party OSS license information. They call the /api/meta and /api/licenses backend endpoints (implemented in the backend task). This sub-task adds API helper functions and creates both page components with tests.

Steps

  1. Edit open-git/lib/api-types.ts — add two new interfaces: AppMeta { app_name: string; version: string; git_commit: string; build_date: string; license: string; source_url: string; } and LicenseEntry { name: string; version: string; license: string; url: string; } and AppLicenses { app_license: string; third_party: LicenseEntry[]; }
  2. Edit open-git/lib/api.ts — add two exported functions: export async function getAppMeta(baseURL: string): Promise<AppMeta> (fetch ${baseURL}/api/meta, return json) and export async function getAppLicenses(baseURL: string): Promise<AppLicenses> (fetch ${baseURL}/api/licenses, return json); on fetch error, getAppLicenses should catch and return { app_license: "", third_party: [] } matching the PRD degraded-mode requirement
  3. Create open-git/app/about/page.tsx"use client" component; use useEffect + useState to call getAppMeta(process.env.NEXT_PUBLIC_API_BASE_URL ?? "") on mount; display: BRANDING.appName, version from the API response (or "dev" while loading), license name with a link to the /licenses page, and a link back to BRANDING.sourceUrl; show a loading state while fetching
  4. Create open-git/app/licenses/page.tsx"use client" component; call getAppLicenses on mount; render app_license as the project's own license, then render a table/list of third_party entries each showing n

Changes Made

I'll
implement
the
about
/licenses
pages
and
API
helpers
.
First
I'm
reading
the
existing
files
and
patterns
in
the
repo
.

Implement
ing
the
six
files
now
.

Implemented
the
/ about
and
/ lic enses
pages
with
API
helpers
and
tests
.

Changes

lib/api-types.ts — Added AppMeta, LicenseEntry, and AppLicenses interfaces.

lib/api.ts
— Added:

  • getAppMeta(baseURL) — fetches /api/meta and returns JSON
  • getAppLicenses(baseURL) — fetches /api/licenses;
    on error returns { app_license: "", third_party: [] }

app/about/page.tsx — Client component that loads meta on mount, shows BRANDING.appName , version ("dev" while loading), license link to /licenses, and source URL link.

app/licenses/page.tsx — Client
component that loads licenses, shows project license and a third-party table, or the fallback message when third_party is
empty.

Testsabout.test.tsx covers loading state, app name, version, and /licenses link; licenses.test.tsx covers rendered entries and empty fallback
text.

[PURPLE-MODIFIED-FILES]
open-git/lib/api-types.ts
open-git/lib/api.ts
open-git/app/about/page.tsx
open-git/app/licenses/page
.tsx

open
-git
/__
tests
__/
app
/about
.test
.tsx

open
-git
/__
tests
__/
app
/licenses
.test
.tsx

[/
P
UR
P
LE
-M
OD
IFIED
-F
ILES
]

Task ID: 019f0d7d-8aeb-74b9-8674-a65ffb0f5541
Generated by Purple Codens

…page.tsx and app/licenses/page.tsx (with tests)

@orange-codens orange-codens Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍊 Orange Codens レビュー

全体的な実装方針は PRD の要件を満たしており、about/page.tsx での cleanup フラグや getAppLicenses のフォールバック設計など良い配慮が見られる。ただし getAppMeta / getAppLicenses 両関数で HTTP 4xx/5xx エラーが握り潰されるresponse.ok チェック欠落)点と、licenses/page.tsx でアンマウント後の state 更新を防ぐ cleanup がない点が high 相当の問題として存在する。また UI テキストの日本語/英語混在(medium/low)やエラーケースのテスト不足も改善が必要。

🔒 セキュリティ: 本 PR で追加された getAppMeta / getAppLicensesbaseURL を検証なく fetch に渡しており、サーバーサイドで利用された際に SSRF となりうる設計上のリスクがあります。また、ライセンスページでは API レスポンス由来の entry.url を無検証で href にセットしており、バックエンド侵害時に javascript: URI 等を通じた XSS / フィッシングが成立します。加えて HTTP エラーレスポンスを ok チェックなしに JSON パースするため、エラー詳細が意図せずクライアントへ露出する可能性があります。

🔴 critical: 1 / 🟠 high: 4 / 🟡 medium: 4 / 🔵 low: 2

その他の指摘 (6 件)
  • 🟡 [medium] lib/api.ts:260 — HTTP エラーレスポンスを検証せずに JSON パースしており、エラー内容がクライアントに漏洩する恐れがある (confidence: 0.90, sec.sast.validation.missing_http_error_check)
  • 🟡 [medium] app/about/page.tsx:34 — ロード失敗時にも version が 'dev' を表示し、エラーが区別できない (confidence: 0.85, code.logic.loading_state_incorrect)
  • 🟡 [medium] __tests__/app/about.test.tsx:1 — about.test.tsx に API エラーケースのテストがない (confidence: 0.85, maintainability.test.error_case_missing)
  • 🟡 [medium] app/licenses/page.tsx:39 — loading=false 後の licenses?.third_party に不要な optional chaining がある (confidence: 0.78, code.logic.optional_chaining_unnecessary)
  • 🔵 [low] app/licenses/page.tsx:40 — UI テキストの言語が混在している (confidence: 0.95, maintainability.i18n.mixed_language)
  • 🔵 [low] app/licenses/page.tsx:53 — table row の key に name+version を使うと重複時に一意性が保証されない (confidence: 0.70, maintainability.naming.inconsistent_key_strategy)

head: e02290d | Orange Codens (P1)

Comment thread lib/api.ts
return fetchWithToken<ApiUserSummary>(token, "/api/v3/user");
}

export async function getAppMeta(baseURL: string): Promise<AppMeta> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 [critical] SSRF: 呼び出し元が任意の baseURL を渡せる getAppMeta / getAppLicenses

CWE-918: Server-Side Request Forgery (SSRF)

該当箇所:

export async function getAppMeta(baseURL: string): Promise<AppMeta> {
  const response = await fetch(`${baseURL}/api/meta`);
export async function getAppLicenses(baseURL: string): Promise<AppLicenses> {
  try {
    const response = await fetch(`${baseURL}/api/licenses`);

baseURL は呼び出し元から完全に自由な文字列として渡されます。現状の呼び出しは process.env.NEXT_PUBLIC_API_BASE_URL ?? "" (クライアントサイド) ですが、この関数がサーバーサイドで利用される場合(例: Server Actions・Route Handler・SSR)、攻撃者がリクエストを細工して http://169.254.169.254/ 等の内部エンドポイントや任意の外部ホストへリクエストを誘導できます。また、file:// / gopher:// 等のスキームも許容されます。

推奨対応:

  • baseURL をホワイトリスト検証する、またはサーバー側では環境変数から直接取得して引数経由で外部から注入できない構造にする。
  • 少なくともスキームを https: / http: に限定し、ホスト部を許可リストと照合する。
```ts
const ALLOWED_ORIGINS = new Set([
  process.env.NEXT_PUBLIC_API_BASE_URL ?? "",
].filter(Boolean));

function assertAllowedBaseURL(baseURL: string): void {
  const { origin } = new URL(baseURL);
  if (!ALLOWED_ORIGINS.has(origin)) {
    throw new Error(`Disallowed baseURL: ${baseURL}`);
  }
}

export async function getAppMeta(baseURL: string): Promise<AppMeta> {
  assertAllowedBaseURL(baseURL);
  const response = await fetch(`${baseURL}/api/meta`);
  return response.json() as Promise<AppMeta>;
}

<sub>`sec.sast.ssrf.user_url` | confidence: 0.75</sub>

Comment thread lib/api.ts
}

export async function getAppMeta(baseURL: string): Promise<AppMeta> {
const response = await fetch(`${baseURL}/api/meta`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] getAppMeta が HTTP エラーレスポンスを無視する

該当箇所:

export async function getAppMeta(baseURL: string): Promise<AppMeta> {
  const response = await fetch(`${baseURL}/api/meta`);
  return response.json() as Promise<AppMeta>;
}

fetch はネットワーク到達可能ならば 4xx/5xx でも例外を投げない。response.ok チェックがないため、サーバーが 500 を返したときに JSON パースがエラー JSON を AppMeta として返すか、非 JSON ボディで SyntaxError が発生し、呼び出し元 (about/page.tsx) でハンドリングされないままレンダリングが壊れる。getAppLicenses は try/catch でフォールバックを返しているのに対し、getAppMeta には同様の保護がない。

最低限 if (!response.ok) throw new Error(...) を追加するか、about/page.tsx 側で catch してエラー表示すべき。

@@ -259,4 +259,6 @@
 export async function getAppMeta(baseURL: string): Promise<AppMeta> {
   const response = await fetch(`${baseURL}/api/meta`);
+  if (!response.ok) {
+    throw new Error(`Failed to fetch app meta: ${response.status}`);
+  }
   return response.json() as Promise<AppMeta>;
 }

code.error_handling.http_error_ignored | confidence: 0.92

Comment thread lib/api.ts
return (await response.json()) as AppLicenses;
} catch {
return { app_license: "", third_party: [] };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] getAppLicenses が HTTP エラーレスポンスを握り潰す

該当箇所:

export async function getAppLicenses(baseURL: string): Promise<AppLicenses> {
  try {
    const response = await fetch(`${baseURL}/api/licenses`);
    return (await response.json()) as AppLicenses;
  } catch {
    return { app_license: "", third_party: [] };
  }
}

fetch 自体は HTTP 4xx/5xx で例外を投げない。サーバーが 500 を返した場合、response.json() は(JSON ボディがあれば)成功してしまい、フォールバックに入らない。response.ok チェックを追加することで、HTTP エラー時にも意図したフォールバックが返るようにすべき。

@@ -265,5 +265,8 @@
 export async function getAppLicenses(baseURL: string): Promise<AppLicenses> {
   try {
     const response = await fetch(`${baseURL}/api/licenses`);
+    if (!response.ok) {
+      return { app_license: "", third_party: [] };
+    }
     return (await response.json()) as AppLicenses;
   } catch {
     return { app_license: "", third_party: [] };
   }
 }

code.error_handling.http_error_ignored | confidence: 0.92

Comment thread app/licenses/page.tsx
getAppLicenses(process.env.NEXT_PUBLIC_API_BASE_URL ?? "")
.then(setLicenses)
.finally(() => setLoading(false));
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] licenses/page.tsx がアンマウント後に state を更新する可能性がある

該当箇所:

useEffect(() => {
  getAppLicenses(process.env.NEXT_PUBLIC_API_BASE_URL ?? "")
    .then(setLicenses)
    .finally(() => setLoading(false));
}, []);

about/page.tsx では cancelled フラグで cleanup しているのに、licenses/page.tsx には cleanup 関数が存在しない。ページから素早く離脱した場合、Promise が解決されたタイミングでアンマウント済みのコンポーネントに setLicenses / setLoading が呼ばれ、React の警告 (Can't perform a React state update on an unmounted component) が発生する(React 18 strict mode では開発時に表示される)。about/page.tsx と同様に cancelled フラグを導入すべき。

@@ -12,7 +12,14 @@
   useEffect(() => {
-    getAppLicenses(process.env.NEXT_PUBLIC_API_BASE_URL ?? "")
-      .then(setLicenses)
-      .finally(() => setLoading(false));
-  }, []);
+    let cancelled = false;
+    getAppLicenses(process.env.NEXT_PUBLIC_API_BASE_URL ?? "")
+      .then((data) => { if (!cancelled) setLicenses(data); })
+      .finally(() => { if (!cancelled) setLoading(false); });
+    return () => { cancelled = true; };
+  }, []);

code.null_safety.unmounted_state_update | confidence: 0.88

Comment thread app/licenses/page.tsx
className="text-primary underline"
>
{entry.url}
</a>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [high] Open Redirect / 任意 URL インジェクション: APIレスポンスの entry.url を href に直接使用

CWE-601: URL Redirection to Untrusted Site ('Open Redirect') / CWE-20: Improper Input Validation

該当箇所:

<a
  href={entry.url}
  target="_blank"
  rel="noopener noreferrer"
  className="text-primary underline"
>
  {entry.url}
</a>

entry.url はバックエンドの /api/licenses から返されるサードパーティライセンス情報のフィールドです。このレスポンスが攻撃者によって改ざんされた場合(例: 中間者攻撃、バックエンド侵害)、javascript:alert(1)data:text/html,... など任意のスキームを含む URL がそのまま href にセットされ、XSS やフィッシングに利用されます。rel="noopener noreferrer" はタブナビゲーションの制御には有効ですが、javascript: URI の実行は防げません。

推奨対応:

function isSafeUrl(url: string): boolean {
  try {
    const { protocol } = new URL(url);
    return protocol === "https:" || protocol === "http:";
  } catch {
    return false;
  }
}

レンダリング前に isSafeUrl(entry.url) を検証し、不正な URL はリンクとしてレンダリングしない。

```tsx
{isSafeUrl(entry.url) ? (
  <a href={entry.url} target="_blank" rel="noopener noreferrer" className="text-primary underline">
    {entry.url}
  </a>
) : (
  <span>{entry.url}</span>
)}

<sub>`sec.sast.injection.open_redirect` | confidence: 0.85</sub>

…o /api/meta, keep main's workflow-job-logs test
@zoetaka38
zoetaka38 merged commit 22c2262 into main Jun 29, 2026
11 of 13 checks passed

@orange-codens orange-codens Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍊 Orange Codens レビュー

PRD 要件の骨格(API 型・ヘルパー・About/Licenses ページ・基本テスト)は diff 上そろっていますが、getAppMeta/getAppLicenses の HTTP エラー処理が不十分で、About ページでは reject 時の unhandled rejection、Licenses ページでは third_party 欠落時の TypeError とアンマウント後 setState のリスクがあります。テストは About の重複と API ヘルパー層の未カバーが目立ちます。

🔒 セキュリティ: 本 PR は公開向け about/licenses ページと API ヘルパーの追加であり、認可漏れ・SSRF・インジェクション系の直接 exploitable な問題は diff 上は見当たりません。一方で API 応答の URL を検証せず href にバインドしており、バックエンド改ざん時に DOM ベース XSS へ発展しうるデータフローが新設されています。http/https スキーム限定の検証を href 設定前および API レイヤー双方に追加することを推奨します。

🟡 medium: 8 / 🔵 low: 3

その他の指摘 (11 件)
  • 🟡 [medium] lib/api.ts:658 — getAppMeta が HTTP エラーレスポンスを検証していない (confidence: 0.92, code.error_handling.missing_response_ok)
  • 🟡 [medium] app/licenses/page.tsx:35 — third_party 欠落時に TypeError でクラッシュする (confidence: 0.90, code.null_safety.optional_chaining)
  • 🟡 [medium] app/about/page.tsx:17 — getAppMeta 失敗時に unhandled rejection が発生する (confidence: 0.88, code.error_handling.unhandled_rejection)
  • 🟡 [medium] app/licenses/page.tsx:12 — Licenses ページにアンマウント時の setState ガードがない (confidence: 0.86, code.react.unmount_setstate)
  • 🟡 [medium] lib/api.ts:663 — getAppLicenses の degraded mode が HTTP エラーをカバーしない (confidence: 0.82, code.error_handling.http_not_degraded)
  • 🟡 [medium] lib/api.ts:658 — getAppMeta/getAppLicenses の API ヘルパー単体テストがない (confidence: 0.78, maintainability.test.missing_api_tests)
  • 🟡 [medium] app/about/page.tsx:38 — API 由来の source_url を href に未検証で設定 (confidence: 0.72, sec.sast.injection.xss)
  • 🟡 [medium] app/licenses/page.tsx:56 — サードパーティ URL を href に未検証で設定 (confidence: 0.70, sec.sast.injection.xss)
  • 🔵 [low] __tests__/pages/about.test.tsx:1 — About ページのテストが2ファイルに重複している (confidence: 0.95, maintainability.test.duplicate)
  • 🔵 [low] __tests__/app/licenses.test.tsx:39 — Licenses テストが app_license 表示を検証していない (confidence: 0.85, maintainability.test.incomplete_coverage)
  • 🔵 [low] lib/api.ts:658 — getAppMeta/getAppLicenses が API 応答を検証せず返却 (confidence: 0.65, sec.sast.input_validation.missing)

head: d80e728 | Orange Codens (P1)

zoetaka38 added a commit that referenced this pull request Jun 29, 2026
@zoetaka38
zoetaka38 deleted the feature/019f0d7d8aeb-019f0d7d8aeb branch July 2, 2026 10:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant