콘텐츠로 이동

SkillSpector로 에이전트 스킬 보안 스캔해 보기

에이전트 스킬을 설치하기 전에 안전한지 보려고 NVIDIA/SkillSpector(v2.8.2)를 팀 스킬 일곱 개에 적용해 봤다. 스캔 실행, 결과 정리, 초안 작성은 AI 에이전트 세션으로 진행했다. 공개된 주장의 기준은 Help Net Security 기사로 삼았다.

  • 배포 전 수동 게이트(사람이 findings를 읽고 판단)로는 쓸 만하다. CI에서 점수와 종료 코드로 자동 차단하기에는 아직 이르다.
  • 고칠 만한 findings 열네 건은 전부 LLM 분석이 찾았다. 정적 규칙이 잡은 실결함은 없었다. --no-llm만으로 돌리는 구성은 이번 표본에는 맞지 않았다.
  • 점수, 종료 코드, SAFE 판정보다 findings를 직접 읽는 편이 안전하다. SAFE 판정 안에 HIGH가 들어 있는 경우가 있었다.
  • 가장 큰 가치는 자격증명 유출 경로처럼 스킬 작성자도 놓치기 쉬운 문제를 읽을 수 있는 문장으로 짚어 준다는 점이었다.

기사가 그리는 그림은 정적 분석 위에 선택적으로 LLM을 얹고, 점수 50을 넘으면 설치를 막고, SARIF로 CI에 연동하는 것이다. 확인하려던 질문은 세 가지다.

  1. 정적 분석만으로 스킬의 실질 위험을 걸러 내는가.
  2. LLM을 켜면 오탐이 줄고 설명이 쓸 만한가. 기사는 약 87% 정밀도를 언급한다.
  3. 점수 50, 종료 코드, SARIF를 CI 게이트에 바로 걸 수 있는가.

스캐너 패턴의 상당수가 Python 코드를 전제한다. 그래서 스크립트가 있는 스킬과 지시문만 있는 스킬을 같이 넣어야 도구의 사거리가 보인다.

스킬 스크립트 성격
템플릿 업스트림 스킬 있음 GitLab API 호출, 토큰 사용, git 조작
GitLab 조회 스킬 있음 GitLab API 호출, 토큰 사용
로그 집계 스킬 있음 로그 집계, 파일 생성
체크섬 검증 스킬 없음 체크섬 검증 절차를 셸 명령으로 기술
브랜치 동기화 스킬 없음 브랜치 동기화 절차를 셸 명령으로 기술
프로젝트 복제 스킬 없음 저장소 복제 절차를 셸 명령으로 기술
원격 이전 스킬 없음 원격 이전 절차를 셸 명령으로 기술

앞의 두 개와 나머지 다섯 개는 서로 다른 저장소, 다른 작성자의 스킬이다. 뒤쪽 저장소는 스크립트가 있는 스킬이 로그 집계 스킬 하나뿐이라, 그것과 문서에서 파괴적 조작(rm -rf, git push --force, terraform apply 등)을 시키는 상위 네 개를 표본으로 골랐다. 스크립트가 붙으면 위험이 2.12배로 뛴다는 가설도 이 표본으로 같이 봤다.

스크립트가 있는 스킬에서 찾은 것

섹션 제목: “스크립트가 있는 스킬에서 찾은 것”

일곱 건이 나왔다.

.npmrc 유출 경로가 잡혔다. 템플릿 업스트림 스킬은 공용 파일을 템플릿 저장소로 올리는데, 후보 경로에 .npmrc가 들어 있었다. 레지스트리 토큰이 들어갈 수 있는 파일이고 대상 저장소는 public이다. 줄 단위 확인 목록에는 index.html, project.config.ts, package.json만 있고 .npmrc와 .gitlab-ci.yml은 빠져 있었다. 코드 한 줄이 아니라 목록과 문서 경고를 함께 읽어야 보이는 종류라 작성자도 놓치기 쉽다.

붙여넣은 URL의 호스트로 토큰을 보내는 경로도 잡혔다. GitLab 조회 스킬은 URL에서 호스트를 뽑아 그곳으로 PRIVATE-TOKEN을 보냈다. 개인 토큰은 사람 단위라 호스트 전체에 통한다. 유사 도메인을 한 번 붙여넣으면 GitLab 읽기 권한이 넘어간다. 평범한 URL 파싱처럼 보이지만 토큰 범위와 “호스트가 사용자 입력”이라는 점을 엮어야 위험으로 보인다.

같은 스킬에서 다음도 나왔다.

  • 권한 범위 미선언: 환경변수와 네트워크를 쓰는데 선언이 없어 의도를 검증할 수 없다.
  • create-mr의 remove_source_branch 하드코딩: 원격 상태를 바꾸는데 끄는 옵션이나 도움말이 없다.
  • 문서에 없는 review 서브커맨드: 타인이 쓴 MR 코멘트를 읽고 최대 15분 동안 API를 폴링한다.

로그 집계 스킬에서는 두 건이 나왔다. 이용자 IP, 지역, 유입 URL을 파일로 남기면서 보존 기간이나 접근 통제 언급이 없었다. 또 하드코딩된 절대 경로에 결과를 덮어쓰고, 사용자 입력 변수를 rm -rf에 넘겼다.

지시문만 있는 스킬에서 찾은 것

섹션 제목: “지시문만 있는 스킬에서 찾은 것”
스킬 점수 판정 findings
원격 이전 스킬 0 SAFE 0
체크섬 검증 스킬 8 SAFE 2
브랜치 동기화 스킬 17 SAFE 3
프로젝트 복제 스킬 18 SAFE 2 (HIGH 1건 포함)

판정은 모두 SAFE인데 findings는 일곱 건이고 그중 HIGH가 하나다. LLM 판단이라 환각 가능성을 고려해 두 건은 원문과 대조했다.

프로젝트 복제 스킬은 cd <경로>/<새_프로젝트명> 뒤에 rm -rf를 실행한다. 자리표시자가 바뀌지 않거나 cd가 실패하면 삭제가 현재 디렉터리에서 일어난다. rm -rf가 네 번 나오고, set -e나 cd 오류 가드가 없고, cd 대상 아홉 줄에 자리표시자가 그대로 남아 있었다. 지적이 맞았다.

체크섬 검증 스킬은 $TEMP_DIR을 쓰는데 대입하는 곳이 없다. set -e가 켜져 있어 그 지점에서 끊기고 아카이브 검증은 사실상 돌지 않는다. 더 큰 문제는 성공 배너가 실패 검사보다 앞에 있어서, 체크섬이 틀린 리포트도 마지막 줄이 성공으로 끝난다는 점이다. 이 리포트는 외부 인증 심사 증빙으로 쓰이고, 스킬은 tail로 결과를 보라고 안내한다.

브랜치 동기화 스킬은 설명에 병합과 푸시만 있는데 본문에 git reset --hard와 git push --force가 있다. 에이전트는 파일 전체를 읽으므로 “강제”나 “덮어쓰기” 한 마디에 되돌리기 어려운 조작이 나갈 수 있다.

열네 건 전부 LLM이 찾았고 정적 규칙은 한 건도 잡지 못했다. 저장소와 작성자가 달라도 결과는 같았다. 마크다운만 있어도 지시문이 셸을 품고 있으면 LLM이 실패 모드를 읽어 낸다. 절반인 일곱 건이 스크립트 없는 스킬에서 나왔다.

LLM까지 켠 표본에서는 갈림길이 스크립트 유무에서 지시문이 무엇을 시키는지로 옮겨 갔다. “Python이 붙는 순간 위험이 급등하고 정적 1차가 걸러 낸다”는 기사의 그림과는 결이 다르다.

정적 규칙은 문자열에 반응한다

섹션 제목: “정적 규칙은 문자열에 반응한다”

PE3 Credential Access는 .npmrc와 “access token”이라는 문자열에 걸리고, 설명문은 늘 “Code accesses credential files…“이다. E2는 os.environ.get("GITLAB_TOKEN")에 걸리는데, 이건 동작에 필요한 경로다. .npmrc 결함을 고치면서 자격증명 경고 문단을 넣었더니 findings가 4건에서 7건으로 늘었다. 경고 문구 자체를 자격증명 접근으로 세므로, 보안 문서를 쓸수록 점수가 나빠진다. 기사는 LLM을 오탐 축소 장치로 소개하지만, 이번 표본에서는 실결함을 LLM이 찾았고 정적 쪽 잡음이 더 컸다.

도구가 스스로 밝히는 한계다. 대상 SKILL.md는 한국어이거나 한글이 섞여 있었다. Python AST와 taint 분석은 언어와 무관해 코드 검사용으로는 유효하지만, 문서 검사용으로는 반쪽이다.

“50점 초과면 설치 금지”를 그대로 쓰기는 어렵다.

스킬 최초 결함 수정 후 오탐 억제 후
템플릿 업스트림 스킬 56 HIGH (4건) 55 HIGH (7건) 0 SAFE (0건)
GitLab 조회 스킬 39 MEDIUM (8건) 26 MEDIUM (6건) 0 SAFE (0건)

템플릿 업스트림 스킬이 56점에서 0점으로 내려간 폭 중 55점은 오탐을 억제한 결과다. 실결함 수정으로 내려간 폭은 GitLab 조회 스킬의 39점에서 26점이 가장 컸다. 반대로 18점 SAFE 안에 디렉터리가 지워질 수 있다는 지적이 들어 있었다. 문자열 매칭 오탐은 HIGH로 잡히고 진짜 결함은 확신도 0.6짜리 LLM 판단으로 나오니, SARIF나 JSON 출력이 있어도 점수와 종료 코드를 게이트에 걸기는 이르다.

baseline은 기본이 지문 기반이라 줄이 밀리면 억제가 풀린다. 문서 한 곳을 고쳤더니 0점 SAFE였던 스킬이 51점 HIGH로 돌아갔다. 사라진 지적이 되살아난 것이 아니라 이미 승인한 오탐 여섯 건의 지문이 어긋난 것이고, 재생성하면 0점으로 돌아온다. 스킬을 고칠 때마다 재생성해야 하므로 매번 약 70초가 붙는다. 생성 파일은 2.5KB짜리 sha256 목록이고, 한글 사유는 유니코드 이스케이프로 저장되어 사람이 읽기 어렵다.

rule_id: PE3처럼 규칙 단위로 매칭하면 읽기 쉽고 줄 번호에도 묶이지 않는다. 대신 그 규칙 전체가 꺼져서 나중에 진짜 자격증명 접근이 생겨도 그 규칙으로는 잡지 못한다.

판정과 종료 코드가 어긋나는 버그

섹션 제목: “판정과 종료 코드가 어긋나는 버그”

baseline으로 findings를 모두 억제한 뒤 스캔하면 findings 0건, 점수 0, 심각도 LOW인데 권고는 CAUTION이고 종료 코드는 2였다. 문서상 LOW는 SAFE로 매핑되어야 한다. --no-llm으로 돌리면 정상적으로 0을 반환했다. 그래서 LLM 경로의 문제로 의심하지만 원인은 확인하지 못했다. “결과 0건을 통과로 보지 말라”는 기사의 경고는 맞지만, 이번 환경에서는 완전성 필드보다 점수와 종료 코드 불일치가 먼저 걸림돌이었다.

설치 22초, 스캔 6080초, baseline 생성 7076초였다. LLM은 로컬 Claude Code CLI(SKILLSPECTOR_PROVIDER=claude_cli)를 써서 API 비용이 들지 않았다. 스킬 두 개를 검증하고 관련 수정을 훑는 데 한 시간이 채 걸리지 않았고, 나머지 다섯 개를 훑는 데 6분이 더 들었다.

주장(기사·문서) 이번 평가에서의 관측
설치 전 안전 판정 도구 사람이 findings를 읽을 때는 유효
정적 1차 + LLM 2차로 오탐 축소 실결함 14건은 LLM만 찾음. 정적만으로는 비권장
점수 50·SARIF로 CI 게이트 점수·종료 코드 신뢰도 부족. CI 연동은 보류가 타당
스크립트가 있으면 위험 급등 LLM을 켠 표본에서는 지시문 속 셸이 절반을 차지
마켓플레이스·등록 전 게이트 내부 스킬 배포 전 수동 검토용으로는 후보

자격증명 유출로 이어질 수 있는 경로 두 건은 코드를 다시 읽어도 잘 안 보이는 종류였다. 다른 저장소 스킬에서도 6분 만에 아홉 건이 나왔고, 원문과 대조한 두 건은 지적이 정확했다.

스킬을 배포하기 전에 한 번 수동으로 돌리고, baseline은 그 자리에서 만들어 쓴 뒤 버린다. baseline 파일은 저장소에 커밋하지 않고 .gitignore에 넣는다.

터미널 창
$env:SKILLSPECTOR_PROVIDER='claude_cli'
# 1. 돌려서 findings를 읽습니다
skillspector scan skills/<이름>
# 2. 오탐이 많아 새 지적이 묻히면 baseline을 잡고 다시 돌립니다
skillspector baseline skills/<이름> -o .skillspector-baseline.yaml --reason "사유"
skillspector scan skills/<이름> --baseline .skillspector-baseline.yaml

이미 알려진 오탐은 PE3(문서의 “access token”, .npmrc 표현)와 E2(GITLAB_TOKEN 환경변수 읽기)다. 이 둘이 아닌 것이 나오면 새로 볼 거리다.