์ปจํ
์คํธ ๋ธ๋ก
| Key |
Value |
| Category |
frontend |
| Checklist |
ISS-UI-R9 โ Use of window/document/localStorage without SSR safety / security |
| Priority |
P1 ๐ |
| Scan Date |
2026-04-16 |
| Flagged By |
@code-review |
์์ฝ
- WHAT:
ApiKeyLoginForm์ด document.cookie๋ก API ํค๋ฅผ ์ง์ ์ฐ๋ฉด์ httpOnly ํ๋๊ทธ๊ฐ ์์ + ConnectionList๋ localStorage.setItem(\"lg:chat:apiKey\", ...)๋ก ํ๋ฌธ ์ ์ฅ
- WHY: XSS ์ทจ์ฝ์ ์ด ๋ฐ์ํ๋ฉด LangSmith API ํค๊ฐ
document.cookie ๋๋ localStorage ํตํด ์ฆ์ ์ ์ถ. switchConnection์ server action ํตํด httpOnly ์ฟ ํค๋ฅผ ์ฌ์ฉํ๋ ์์ ํ ๊ฒฝ๋ก๊ฐ ์ด๋ฏธ ์๋๋ฐ, ApiKeyLoginForm์ ์ด๋ฅผ ์ฐํ
- WHERE:
frontend/src/app/(auth)/login/ApiKeyLoginForm.tsx:71, frontend/src/shared/components/settings/ConnectionList.tsx:113
- SEVERITY: HIGH โ XSS exfiltration vector for paid LangSmith API key
Evidence
| # |
File |
Line |
Finding |
Flagged By |
Confidence |
| 1 |
frontend/src/app/(auth)/login/ApiKeyLoginForm.tsx |
71 |
document.cookie = \lg_apiKey=...; samesite=lax`โhttponly` ํ๋๊ทธ ์์, JS์์ ์ฝ๊ธฐ ๊ฐ๋ฅ |
@code-review |
High |
| 2 |
frontend/src/shared/components/settings/ConnectionList.tsx |
113 |
localStorage.setItem(\"lg:chat:apiKey\", connection.apiKey) โ ํ๋ฌธ localStorage ์ ์ฅ |
@code-review |
High |
| 3 |
(๋์กฐ) frontend/src/app/actions.ts updateConnectionAction |
โ |
server action ๊ฒฝ๋ก๋ httpOnly: true๋ก ์์ ํ๊ฒ ์ค์ (์ฐธ๊ณ : ์์ ํ ๊ฒฝ๋ก ์กด์ฌ) |
@code-review |
High |
์ํฅ ๋ถ์
์ํฅ ๋ฒ์
- API key ์ธ์ฆ ๋ชจ๋(
api-key) ์ฌ์ฉ ๋ชจ๋ ์ฌ์ฉ์
- ConnectionList๋ก ๋ค์ค ์ฐ๊ฒฐ ๊ด๋ฆฌํ๋ ๋ชจ๋ ์ฌ์ฉ์
- XSS ์ทจ์ฝ์ ๋ฐ์ ์ ์ฆ์ ํค ํ์ทจ โ LangSmith ๊ฒฐ์ ํญํ, ํ ํ๋ก์ ํธ trace ์ ๊ทผ
์ฅ์ ์๋๋ฆฌ์ค
- ์ฌ์ฉ์๊ฐ admin ์ฝ์์ด๋ third-party ์์กด์ฑ์์ XSS ํ์ด๋ก๋ ์คํ
- ํ์ด๋ก๋๊ฐ
document.cookie ๋๋ localStorage[\"lg:chat:apiKey\"] ์ฝ์
- ์ธ๋ถ ์๋ฒ๋ก ํค ์ ์ก
- ๊ณต๊ฒฉ์๊ฐ ํค๋ก LangSmith API ํธ์ถ, trace ๋ค์ด๋ก๋, ๋น์ฉ ๋ฐ์
๊ธด๊ธ๋
์ ์ ํด๊ฒฐ ๋ฐฉ์
์ ๊ทผ ๋ฐฉ๋ฒ
ApiKeyLoginForm: document.cookie ์ง์ ์ฐ๊ธฐ ์ ๊ฑฐ, ๊ธฐ์กด server action ํตํ updateConnectionAction ํธ์ถ๋ก ํต์ผ:
// ๋ณ๊ฒฝ ์ (line 71)
document.cookie = \`lg_apiKey=\${connection.apiKey}; path=/; samesite=lax\`;
// ๋ณ๊ฒฝ ํ
import { updateConnectionAction } from \"@/app/actions\";
await updateConnectionAction({
apiUrl: connection.apiUrl,
assistantId: connection.assistantId,
apiKey: connection.apiKey,
});
ConnectionList: localStorage.setItem(\"lg:chat:apiKey\", ...) ์ ๊ฑฐ (์ด๋ฏธ server action์ด httpOnly ์ฟ ํค ์ฒ๋ฆฌ). API ํค๋ฅผ ํด๋ผ์ด์ธํธ์์ ๋ค์ ์ฝ์ด์ผ ํ๋ค๋ฉด server action getConnectionAction() ํธ์ถ.
๋์
- httpOnly: false ์ ์งํ๋ ์งง์ ๋ง๋ฃ: ๋ง๋ฃ ๋จ์ถ์ผ๋ก๋ XSS ๋
ธ์ถ ์๊ฐ ์ค์ด๋ค์ง๋ง 1ํ ํ์ทจ๋ก๋ ์ถฉ๋ถ โ ๋ฏธํด๊ฒฐ
- ์ํธํ ์ ์ฅ: ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์ํธํ๋ ํค ์์ฒด๊ฐ ํด๋ผ์ด์ธํธ์ ์์ผ๋ฏ๋ก ์๋ฏธ ์์
์์ฉ ๊ธฐ์ค
์ฐธ์กฐ
์ฌํ ๋ฐฉ๋ฒ
์ฌ์ ์กฐ๊ฑด
- API key ์ธ์ฆ ๋ชจ๋๋ก ๋ก๊ทธ์ธ
๋จ๊ณ
- ๋ธ๋ผ์ฐ์ ์์ ApiKeyLoginForm ํตํด ํค ์
๋ ฅ ํ ๋ก๊ทธ์ธ
- DevTools Console์์
document.cookie ํ์ธ
localStorage.getItem(\"lg:chat:apiKey\") ํ์ธ
๊ธฐ๋ ๊ฒฐ๊ณผ
๋ ๋ค ํค ๋
ธ์ถ ์์ (server action์ด httpOnly ์ฟ ํค๋ก ์ ์ฅ)
์ค์ ๊ฒฐ๊ณผ
document.cookie์ lg_apiKey=sk-... ํ๋ฌธ ๋
ธ์ถ, localStorage์๋ ๋์ผ
๊ด๋ จ ์ฝ๋ ์ปจํ
์คํธ
| File |
Role |
Relevance |
frontend/src/app/(auth)/login/ApiKeyLoginForm.tsx |
API key ์ธ์ฆ ํผ |
์์ ๋์ |
frontend/src/shared/components/settings/ConnectionList.tsx |
์ฐ๊ฒฐ ๊ด๋ฆฌ UI |
์์ ๋์ |
frontend/src/app/actions.ts |
server actions (httpOnly ์ฟ ํค ์ฌ์ฉ) |
ํธ์ถ ๋์ (์ฌ์ฌ์ฉ) |
Detected by oh-my-braincrew `omb:issue` scan
Category: frontend | Scan date: 2026-04-16
`omb-issue-scan category=frontend checklist=ISS-UI-R9`
์ปจํ ์คํธ ๋ธ๋ก
ISS-UI-R9โ Use of window/document/localStorage without SSR safety / security์์ฝ
ApiKeyLoginForm์ดdocument.cookie๋ก API ํค๋ฅผ ์ง์ ์ฐ๋ฉด์httpOnlyํ๋๊ทธ๊ฐ ์์ +ConnectionList๋localStorage.setItem(\"lg:chat:apiKey\", ...)๋ก ํ๋ฌธ ์ ์ฅdocument.cookie๋๋localStorageํตํด ์ฆ์ ์ ์ถ.switchConnection์ server action ํตํด httpOnly ์ฟ ํค๋ฅผ ์ฌ์ฉํ๋ ์์ ํ ๊ฒฝ๋ก๊ฐ ์ด๋ฏธ ์๋๋ฐ, ApiKeyLoginForm์ ์ด๋ฅผ ์ฐํfrontend/src/app/(auth)/login/ApiKeyLoginForm.tsx:71,frontend/src/shared/components/settings/ConnectionList.tsx:113Evidence
frontend/src/app/(auth)/login/ApiKeyLoginForm.tsxdocument.cookie = \lg_apiKey=...; samesite=lax`โhttponly` ํ๋๊ทธ ์์, JS์์ ์ฝ๊ธฐ ๊ฐ๋ฅfrontend/src/shared/components/settings/ConnectionList.tsxlocalStorage.setItem(\"lg:chat:apiKey\", connection.apiKey)โ ํ๋ฌธ localStorage ์ ์ฅfrontend/src/app/actions.tsupdateConnectionActionhttpOnly: true๋ก ์์ ํ๊ฒ ์ค์ (์ฐธ๊ณ : ์์ ํ ๊ฒฝ๋ก ์กด์ฌ)์ํฅ ๋ถ์
์ํฅ ๋ฒ์
api-key) ์ฌ์ฉ ๋ชจ๋ ์ฌ์ฉ์์ฅ์ ์๋๋ฆฌ์ค
document.cookie๋๋localStorage[\"lg:chat:apiKey\"]์ฝ์๊ธด๊ธ๋
httpOnly์ฟ ํค ๊ฒฝ๋ก๊ฐ ์ด๋ฏธ ์ฝ๋์ ์กด์ฌํ๋ฏ๋ก ์์ ๋น์ฉ ๋ฎ์์ ์ ํด๊ฒฐ ๋ฐฉ์
์ ๊ทผ ๋ฐฉ๋ฒ
ApiKeyLoginForm:
document.cookie์ง์ ์ฐ๊ธฐ ์ ๊ฑฐ, ๊ธฐ์กด server action ํตํupdateConnectionActionํธ์ถ๋ก ํต์ผ:ConnectionList:
localStorage.setItem(\"lg:chat:apiKey\", ...)์ ๊ฑฐ (์ด๋ฏธ server action์ด httpOnly ์ฟ ํค ์ฒ๋ฆฌ). API ํค๋ฅผ ํด๋ผ์ด์ธํธ์์ ๋ค์ ์ฝ์ด์ผ ํ๋ค๋ฉด server actiongetConnectionAction()ํธ์ถ.๋์
์์ฉ ๊ธฐ์ค
document.cookie = \"lg_apiKey=...\"ํจํด ์ ๊ฑฐlocalStorage.setItem(\"lg:chat:apiKey\", ...)์ ๊ฑฐdocument.cookie,localStorage.*apiKey0๊ฑด ํ์ธdocument.cookie์ lg_apiKey ์์ ํ์ธcd frontend && pnpm test์ฐธ์กฐ
frontend/src/app/(auth)/login/ApiKeyLoginForm.tsx,frontend/src/shared/components/settings/ConnectionList.tsx,frontend/src/app/actions.ts์ฌํ ๋ฐฉ๋ฒ
์ฌ์ ์กฐ๊ฑด
๋จ๊ณ
document.cookieํ์ธlocalStorage.getItem(\"lg:chat:apiKey\")ํ์ธ๊ธฐ๋ ๊ฒฐ๊ณผ
๋ ๋ค ํค ๋ ธ์ถ ์์ (server action์ด httpOnly ์ฟ ํค๋ก ์ ์ฅ)
์ค์ ๊ฒฐ๊ณผ
document.cookie์lg_apiKey=sk-...ํ๋ฌธ ๋ ธ์ถ, localStorage์๋ ๋์ผ๊ด๋ จ ์ฝ๋ ์ปจํ ์คํธ
frontend/src/app/(auth)/login/ApiKeyLoginForm.tsxfrontend/src/shared/components/settings/ConnectionList.tsxfrontend/src/app/actions.ts