hallint 업데이트: 수정 사항, 출시 사항 및 v0.2 예정 사항
요약
AI 어시스턴트가 생성하는 보안 버그를 탐지하는 린터인 hallint의 업데이트 소식을 전합니다. 사용자 피드백을 반영하여 오탐지를 줄이기 위한 규칙 수정 및 인라인 억제 마커 기능을 도입했습니다.
핵심 포인트
- async-no-catch 규칙을 기본 권장 세트에서 제거하여 노이즈 감소
- 인라인 마커를 통한 공개 경로(public routes) 인증 체크 제외 기능 추가
- hardcoded-secret 탐지를 위한 이중 패스(dual-pass) 방식 도입
- 사용자 피드백 기반의 실질적인 보안 규칙 정교화
이 글은 AI 어시스턴트가 계속해서 작성하는 보안 버그를 잡아내는 린터(Linter)를 만들었습니다에 대한 후속 글입니다. 해당 게시물의 댓글들이 이번 업데이트 내용의 대부분을 형성했습니다 — 감사합니다.
첫 번째 게시물은 많은 솔직한 피드백을 받았습니다. 단순히 "멋진 프로젝트네요"라는 반응뿐만 아니라, 아직 올바르지 않은 부분들에 대한 실제적인 기술적 반론이 있었습니다. 이 포스트에서는 그 피드백을 바탕으로 무엇을 수정했는지, 그 이후 무엇을 출시했는지, 그리고 v0.2에서는 무엇이 예정되어 있는지를 다룹니다.
현재 버전:
@asyncinnovator/hallint— v0.1.8@asyncinnovator/hallint-cli— v0.1.7
댓글에서 지적된 올바른 점들
두 가지 이슈가 반복적으로 제기되었으며, 둘 다 타당했습니다.
1. async-no-catch가 신호(signal)를 오염시키고 있었습니다.
Nazar Boyko가 명확하게 설명했습니다: 이 규칙은 "플래그(flagged)된 줄을 보는 사람이 그것이 틀렸다는 것에 동의한다"라는 기준을 깨뜨립니다. 호출자(caller)가 처리하거나, Express 에러 미들웨어(error middleware)가 상위 단계에서 처리하기 때문에 try/catch가 없는 올바른 비동기(async) 코드는 매우 많습니다. 실제 코드베이스에서 hallint를 실행했을 때, 하나의 진짜 하드코딩된 키(hardcoded key) 옆에 40개의 async-no-catch 경고가 뜨는 것은 여러분이 구축하려 노력한 신호(signal)를 망가뜨립니다.
출시된 수정 사항: async-no-catch는 이제 recommended 규칙 세트에서 제거되었습니다. --rules all 옵션을 통해 선택적으로 사용하면 여전히 존재하고 실행되지만, 더 이상 기본적으로 발생하지는 않습니다. "이 줄은 틀렸다"라는 속성을 공유하는 7개의 보안 규칙은 여전히 recommended에 남아 있습니다.
2. missing-auth-check가 의도적으로 공개된 경로(public routes)에서 오탐(false positive) 문제를 일으켰습니다.
Dipankar Sarkar와 Adam Lewis 모두 이 문제를 지적했습니다. 헬스 체크(Health checks), 웹훅(webhooks), 공개 엔드포인트(public endpoints) 등은 인증 미들웨어(auth middleware)가 없어야 합니다. 이것들을 플래그 처리하는 것은 노이즈(noise)이며, 노이즈가 발생하면 규칙은 비활성화됩니다.
출시된 수정 사항: hallint는 이제 세 가지 인라인 억제 마커(inline suppression markers)를 인식합니다:
// public
// hallint-public
/* hallint-public */
이 중 하나를 라우트(route)에 추가하면 hallint는 해당 핸들러(handler)에 대한 인증 확인(auth check)을 건너뜁니다. 깔끔하고 명시적이며, 마법 같은 추론이 필요하지 않습니다.
app.get('/health', (req, res) => { // hallint-public
res.json({ status: 'ok' })
})
이번 릴리스 사이클의 기타 수정 사항
hardcoded-secret이 이제 이중 패스(dual-pass) 탐지를 수행합니다.
기존 정규 표현식(regex)은 일반적인 api_key = "..." 패턴을 포착했습니다. 하지만 변수 이름과 상관없이 명백한 증거가 되는 토큰 접두사(token prefixes)는 놓쳤습니다. 이제 이 규칙은 알려진 접두사인 ghp_, sk-, AKIA, xoxb-를 대상으로 하는 두 번째 패스를 가집니다. 변수 이름이 무해해 보이더라도, 이 중 하나를 포함하는 라인은 심각한(critical) 상태로 플래그가 지정됩니다. 주석과 환경 변수(env-var) 할당은 제외됩니다.
sqlInjection 메시지 문구가 과도하게 주장되었습니다.
기존 메시지는 "쿼리로 흐르는 사용자 입력(user input flowing into query)"이라고 명시했으나, 탐지 방식은 데이터 흐름(data-flow) 기반이 아닌 패턴 기반이었습니다. 즉, 변수가 실제로 사용자 입력인지 증명할 수는 없었고, 단지 쿼리 호출 내에서 템플릿 리터럴(template literal)이 사용되었다는 것만 알 수 있었습니다. 이제 메시지는 규칙이 실제로 탐지하는 내용, 즉 쿼리 호출 내의 템플릿 리터럴 보간(interpolation)을 반영합니다. 정확한 범위를 지정하여 잘못된 확신을 주지 않습니다.
스캐너 디스패치(Scanner dispatch)가 리팩터링되었습니다.
이전에는 스캐너가 정규 표현식(regex)을 실행할지 AST 매칭을 실행할지 결정하기 위해 rule.layer를 사용했습니다. 이는 규칙이 layer를 올바르게 설정하지 않으면 조용히 건너뛰어질 수 있음을 의미했습니다. 이제 rule.match()의 존재 여부가 디스패치 신호가 됩니다. 규칙에 match() 함수가 있으면 AST 처리를 수행하고, 그렇지 않으면 정규 표현식을 사용합니다. layer는 이제 문서화 및 필터링에 사용되는 메타데이터로만 남습니다. 이를 통해 규칙 작성 시 오류가 발생할 가능성이 줄어듭니다.
asyncNoCatch 탐지가 취약했습니다.
비동기 함수(async function)의 경계를 찾기 위해 사용했던 중괄호 카운터(brace-counter) 방식은 중첩된 객체(nested objects)나 중괄호가 포함된 템플릿 리터럴에서 작동하지 않았습니다. 이를 카운팅 전에 문자열 내용을 제거하는 stripStrings() 휴리스틱(heuristic)으로 교체했습니다. 이 규칙이 recommended에서 제외되기 전부터 이미 오탐(false positives)이 현저히 줄어들었습니다.
GitHub Actions 지원
이제 단 다섯 줄의 코드로 hallint를 통한 CI 게이팅(gate CI)을 설정할 수 있습니다:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
...
hallint는 심각(critical)하거나 높은(high) 위험도가 발견되면 종료 코드(exit code) 1을 반환하므로, 실제 문제 발생 시 머지(merge)를 차단합니다. 향후 도입될 LLM 레이어는 절대로 종료 코드 1을 트리거하지 않을 것입니다. 비결정론적(Non-deterministic) 체크는 CI 게이트(CI gates)에 적합하지 않기 때문입니다.
v0.2 예정 사항
댓글에서 약속되었던 몇 가지 기능들이 현재 활발히 진행 중입니다.
파일 간 라우터 구성 추적 (Cross-file router composition tracking).
현재 missing-auth-check는 동일 파일 내에서만 작동합니다. 만약 미들웨어(middleware)가 app.ts에 등록되어 있고 라우트(routes)가 routes/users.ts에 있다면, hallint는 이 둘을 연결하지 못합니다. 이것이 정규 표현식(regex) 기반 분석의 솔직한 한계입니다. 즉, 임포트(import)를 따라갈 수 없습니다. v0.2에서는 AST(Abstract Syntax Tree) 분석을 위해 tree-sitter를 도입하여 파일 간 추적을 가능하게 할 예정입니다. 이것이 v0.2의 주요 구조적 변화입니다.
LLM 레이어 — 선택 사항(opt-in) 전용.
선택적 LLM 리뷰 단계는 v0.2에서 --llm ollama 또는 --llm anthropic과 같은 엄격한 선택 사항(opt-in) 방식으로 도입될 것입니다. 기본적으로 실행되지 않으며, 종료 코드 1에 영향을 주지 않을 것입니다. Kartik가 댓글에서 언급했듯이, CI 게이트에서의 비결정론(non-determinism)은 결격 사유입니다. LLM 레이어는 정규 표현식 및 AST 레이어가 놓치는 것들 — 의미론적 문제(semantic issues), 논리적 결함(logic flaws), 문맥을 고려하지 않은 패턴(context-blind patterns) — 을 드러내지만, 이러한 결과물은 머지를 차단하는 메인 결과가 아닌 별도의 출력 섹션에 표시됩니다.
v0.2–v0.3에서 고려 중인 새로운 규칙들:
silent-error-swallow— 로깅이나 재발생(rethrowing) 없이 에러를 삼켜버리는 빈 catch 블록. Edu Peralta가 설명한 패턴으로, 처리된 것처럼 보이지만 실제로는 그렇지 않은 경우입니다.jwt-in-localstorage— XSS(Cross-Site Scripting) 공격이 접근할 수 있는 곳에 토큰을 저장하는 경우
- hallucinated-dependency — package.json에 존재하지 않는 패키지를 임포트하는 경우 (AI 특유의 실제 실패 모드)
기여하기 (Contributing)
각 규칙은 bad.ts와 good.ts 피스처(fixture)를 포함한 약 30줄 정도의 단일 파일로 구성됩니다. 만약 AI 어시스턴트가 계속해서 생성하지만 hallint가 아직 잡아내지 못하는 패턴을 발견했다면, "이것을 발견했다"에서 "수정 사항을 배포했다"까지의 과정은 매우 짧을 것입니다.
good first issue로 라벨이 지정된 이슈들은 범위가 미리 정해져 있어 바로 작업에 착수할 수 있습니다.
GitHub: github.com/Asyncinnovator/hallint
npx @asyncinnovator/hallint-cli ./src
MIT 라이선스 (MIT licensed). 개인 및 상업적 용도로 무료로 사용할 수 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기