올해 5월부터 개인적으로 '오스소구라(おすそぐら)'라는 방재 비축 서비스를 만들고 있습니다. 얼마 전, OWASP ASVS 5.0의 레벨 1과 2, 총 253개 항목을 각각 한 건씩 점검했습니다. AI도 사용하면서 요구사항을 읽고, 코드를 조사하고 설정을 확인하며 판정 결과를 기록 파일에 남겼습니다. 그 경위는 지난 글에 적었습니다.
글의 마지막은 이렇게 마무리되었습니다.
한 번 하고 끝나는 것이 아니기 때문에, 구간별로 같은 도메인을 한 바퀴 돌기로 했습니다. 이번 판정은 각각 하나씩 기록했기 때문에, 다음에는 차이점만 보면 됩니다.
전작을 썼을 당시에는, 차이점을 이용한 재점검을 아직 시도해 보지 못했습니다. 그 후에, ASVS 전용 Claude Code Skill이 몇 개 공개된 것을 알게 되었습니다. 요구사항의 원문(原文)을 가지고 있고, 코드를 읽어 판정까지 내릴 수 있는 것입니다.
그중 하나를 사용해서 같은 챕터를 조건 변경을 통해 4번 점검했습니다.
결과를 먼저 말씀드리자면, 재점검은 짧게 할 수 있었습니다. 다만, 같은 Skill을 사용하더라도 무엇을 입력하여 실행시키는지에 따라 발견되는 문제가 달라졌습니다. 가장 비용이 적게 드는 조건에서는 이전 회차에서 발견했던 결함을 놓쳤습니다.
사용한 Skill과 점검 방법
이번에 사용한 것은 TarkinLarson/asvs-auditor의 agent-asvs입니다.
이 Skill에는 ASVS 5.0의 전체 345개 요구사항이 챕터별로 수록되어 있습니다(OWASP 원문에서 생성했으며, CI에서 차이점을 감시하고 있습니다).
선택한 이유는 두 가지가 있습니다. 첫째는 PASS / FAIL / N/A 외에 UNVERIFIABLE(코드만으로는 확인할 수 없음)이라는 판정값이 있다는 점입니다. 둘째는 '확인하지 않은 것과 합격한 것은 다르다'고 SKILL.md에 명시되어 있다는 것입니다.
지난번 점검에서는 미달성 27건 중 12건이 문서 관련 요구사항이었습니다. 코드를 읽어도 판단할 수 없는 요구사항을 이 Skill이 어떻게 처리하는지 보고 싶었습니다.
대상은 세션 관리 챕터로만 한정했습니다. 18개 요구사항입니다. 전체 17개 챕터를 돌기 전에, 1개 챕터에서 성격과 비용을 보고 싶었습니다.
이번에는 같은 18개 요구사항을 4가지 조건으로 점검했습니다. ①처음부터 전면 점검, ②지난 기록과 차이점 사용, ③Bash를 금지, ④Bash를 금지한 상태로 의존 코드를 전달합니다. 결과는 후반에 모아서 비교하겠습니다.
Skill을 넣어 1개 챕터만 점검하기
agent-asvs는 Claude Code에서 사용하는 Skill이므로, 리포지토리의 .claude/skills/에 두면 읽힙니다.
git clone https://github.com/TarkinLarson/asvs-auditor.git /tmp/asvs-auditor
# 이 글에서 사용한 버전을. 최신으로 시도할 경우 생략합니다.
git -C /tmp/asvs-auditor checkout b426297
...
b426297은 2026-09-24의 커밋입니다. 요구사항 데이터는 OWASP 원문에서 생성되어 업데이트되므로, 결과를 비교하려면 Skill 측의 커밋도 기록해 두는 것이 좋습니다.
대화가 아닌 claude -p로 한 번 실행해서 결과를 받는 방식으로 했습니다.
claude -p
임시 worktree에 배포하여 판정 파일, 감사 기록, 대응 계획, 그리고 기사 초안을 삭제한 후 실행했습니다(커밋은 하지 않았습니다). 반면 요구사항을 작성한 파일은 남겨두었습니다. 그 파일들은 ASVS가 '작성되어 있는지'를 묻는 대상 자체이므로, 지우면 문서 요구사항의 판정이 부당하게 엄격해집니다.
Skill은 삭제 사실을 인지하고 보고서 서두에 "이것들은 작업 트리에 삭제되었으므로 증거로 인용할 수 없고 사용하지 않았다"라고 스스로 작성했습니다.
### 놓친 부분이 2건 발견됨
12분 01초. 제가 미달로 판정했던 7개 항목은 모두 찾아냈습니다. 문서 요구사항 4개도 포함해서요. 오탐(誤検出)은 보이지 않았습니다.
**그리고, 제가 '만족한다'고 판정했던 요구사항 중 2개를 만족하지 못한다고 지적했습니다.**
구체적인 내용은 밝히지 않고, 놓친 원인만 적겠습니다.
**1건은, 요구사항을 짧게 베껴 쓰는 순간 조건이 떨어져 있었습니다.** 판정 파일에는 요구사항을 1줄로 요약하여 작성했습니다. 그 요약에서 요구사항의 후반부가 사라졌습니다. 전반부만 보면 판정은 맞습니다. **요약을 작성하는 시점에서, 확인 대상 자체가 바뀌었던 것입니다.**
**또 다른 1건은, 여러 인증 경로 중 일부만 확인했기 때문입니다.** 확인한 경로에서는 만족했지만, 다른 경로에는 같은 대책이 없었습니다.
둘 다 원문의 요구사항을 생략하지 않고 가지고 있었고, 양쪽 경로를 동일한 기준으로 봤기 때문에 발견할 수 있었습니다.
## 이전 기록과 차이점을 사용해 재점검하기
이 Skill에는 이전 기록과 차이점을 사용해 재점검하는 전용 메커니즘이 준비되어 있지 않았습니다. 그래서 제가 직접 조합하기로 했습니다.
이전 판정 기록을 읽게 하고, `git diff`를 보여주며, 요구사항별로 재확인이 필요한지 판단하게 하고, 필요한 것만 다시 확인시켰습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
agent-asvs를 사용해서 ASVS의 V7을 재점검해 주세요. 새로운 감사는 아닙니다.
이전 점검 기록(docs/security-asvs-*.md)과,
이전 대상 커밋부터 현재까지의 git diff를 봐주세요.
...
Skill에는 요구사항 원문과 판정 절차가 준비되어 있었고, 재점검 범위 판단 규칙은 제가 추가했습니다. 요구사항별로 재확인이 필요한지 결정하는 것은 Skill 측입니다. 효과가 있는 부분은 4행부터 6행까지인데, **보지 못한 요구사항을 PASS로 올리지 못하게 하는** 제약이 없으면 효율화는 그대로 '안 본 것을 통과시키는 것'이 될 것입니다.
대상은 25 커밋이었고, 앱 및 구성으로 40 파일 2,448행의 추가가 있었습니다.
실행 시간은 12분 01초에서 6분 25초로 단축되었습니다. API 환산 비용도 47% 감소했습니다.
하지만, 확인하는 요구사항이 크게 줄어든 것은 아닙니다. 18개 중 17개를 재확인했습니다.
이번 차이점에는 이전 점검에서 발견된 문제의 수정 사항이 많이 포함되어 있습니다. 세션 관련 구현과 방침 문서를 다시 작성했기 때문에, 이 장의 거의 모든 요구사항이 재확인 대상이 되었습니다.
**그럼에도 단축된 것은, 이전 기록으로부터 어떤 파일을 조사해야 할지 알았기 때문입니다.**
## Bash를 금지하고 재점검하기
다음으로, Skill이 어떤 상태로 작동하는지 확인했습니다. 공개된 Skill을 그대로 자신의 리포지토리에서 실행했으므로, 어떤 도구를 사용할 수 있는지 파악하고 싶었습니다.
이 Skill에는 사용하는 도구로 `Read` / `Grep` / `Glob`가 지정되어 있습니다. 그런데 실제 점검에서는 Bash가 34번 사용되었습니다.
설정 파일의 허용 목록에 `sed`도 `grep`도 `git`도 포함되어 있지 않았습니다. 최소한의 실험으로 분리해 보니, `--allowedTools`는 지정한 도구로만 제한하는 옵션이 아니었습니다. `--allowedTools Read`만 전달해도 Bash가 실행되었고, Bash를 멈추게 하려면 `--disallowedTools Bash`가 필요했습니다. Skill 측의 지정도, Skill 도구를 통한 호출에서는 효과가 없었습니다.
하지만, Bash를 막아도 '아무것도 볼 수 없는' 것은 아닙니다. 허용된 읽기 도구로 할 수 있는 것은 그대로 할 수 있습니다. 어디까지 읽을 수 있는지는 나중에 문제가 될 것입니다.
Bash를 금지하면 Claude Code에서 `git diff`를 실행할 수 없습니다. 그래서 차이점은 미리 자신이 파일에 저장해 두고, 그것을 읽게 하기로 했습니다.
# 이전 점검 커밋부터 현재까지의 차이점을 먼저 보여주기
mkdir -p audit-input
git diff <base> HEAD > audit-input/changes.patch
...
`<base>`
은 지난번 점검했던 커밋입니다.
4분 21초 동안 Bash 실행은 0회였습니다. Skill 스스로가 'shell이 없다는 것이 장애가 되지 않았다'고 적었습니다.
하지만 이전 실행에서 발견했던 퇴행(regression)을 놓쳤습니다.
## 의존 코드를 전달하자, 결함을 찾았다
### 놓치고 있던 결함
놓치고 있던 것은 **로그아웃 시 쿠키를 삭제할 수 없게 되어 있었다**는 결함이었습니다.
보안 대응으로 세션 쿠키 이름에 `__Host-` 프리픽스를 붙였습니다. 이 이름의 쿠키는 `Secure`가 붙어있지 않으면 브라우저가 받아들이지 않습니다. 반면, 로그아웃 처리는 `cookies().delete(name)` 그대로였습니다.
`cookies().delete(name)`에 이름만 전달하면 Next.js는 삭제용 `Set-Cookie`에 `Secure`를 붙이지 않습니다. 이 때문에 `__Host-`가 붙은 쿠키의 삭제를 브라우저가 받아들이지 못하고, **쿠키가 남아있었습니다.** 구현은 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
delete(...args) {const [name, options] = typeof args[0] === "string" ? [args[0]] : [args[0].name, args[0]];return this.set({ ...options, name, value: "", expires: new Date(0) });...}```
문자열일 때는 options에 들어갈 것이 없습니다. delete({ name, secure: true, path: '/' })처럼 속성을 전달하면 이어지므로, 문제는 delete() 그 자체가 아니라 이름만 전달했다는 것이었습니다. 실제 수정에서는 속성을 명시한 set()으로 대체했습니다.
서버 측에서는 세션을 무효화하고 있으므로, 남아있는 쿠키로 인증할 수 있는 상태는 아니었습니다. 하지만 브라우저에서 쿠키를 지우는 처리는 작동하지 않았습니다. 보안 대응으로 붙인 __Host-가 다른 대책을 막고 있었습니다.
의존 코드를 두고 다시 실행하기
이 결함을 발견할 수 있었던 것은, Bash로 작업 디렉토리 밖으로 나가서 실제 프로젝트의 node_modules에서 Next.js 구현을 읽었기 때문입니다. 점검용 체크아웃에는 node_modules가 없어서 Bash를 금지하면 읽을 수 없습니다.
그래서 Bash는 금지한 채, 의존 코드를 점검용 트리에 두고 재실행했습니다. 프롬프트에도 구현을 직접 읽을 수 있다는 문장을 한 줄 추가했습니다(어떤 파일을 보라고는 하지 않았습니다).
5분 13초 만에 같은 결함을 발견했습니다. Bash 실행은 0회였습니다. 의존 코드가 읽을 수 있는 곳에 있다면, Bash를 허용하지 않아도 발견할 수 있었습니다. 늘어난 비용은 52초와 $0.26이었습니다. 하지만 바꾼 것은 배치(placement)와 프롬프트 양쪽이므로, 둔 것만이 효과가 있었다고는 할 수 없습니다.
전달한 버전이 달랐다
Skill은 'Next 16.4.0이 실제로 무엇을 내는지 추적한다'고 보고했지만, 전달했던 코드는 16.2.4였습니다. package.json에는 16.4.0이라고 적혀있는데 손안의 node_modules가 오래된 상태였고, Skill은 그 차이를 확인하지 않고 16.4.0인 것처럼 취급했습니다.
Bash로 외부에서 가져온 실행은 스스로 '읽은 것은 16.2.4, 지정은 16.4.0'이라고 한계를 적었습니다. 놓아준 코드에 대해서는 확인하지 않았습니다. 나중에 공개된 16.4.0의 구현을 보니 해당 부분은 같았기 때문에, 이번에는 결론에 영향을 미치지 않았습니다. 그럼에도 불구하고, 의존 코드를 전달할 뿐만 아니라, 그 버전이 맞는지도 전달하는 쪽이 확인해야 한다는 것을 알게 되었습니다.
4번의 비교
⚠️ [IMG:N] 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
| 전면 점검 | 차이점 + Bash 있음 | 차이점 + Bash 금지 | 차이점 + Bash 금지 + 의존 코드 |
|---|
| 소요 시간 | 12 분 01 초 | 6 분 25 초 | 4 분 21 초 |
| API 환산 | $5.43 | $2.89 | $2.22 |
| 재확인한 요구사항 | 18 / 18 | 17 / 18 | 16 / 18 |
| Bash | 34 회 | 28 회 | 0 회 |
| 퇴행 감지 | — | 있음 | 없음 |
금액은 실행 툴이 반환하는 API 환산액이며, 정액 플랜의 추가 청구액이 아닙니다. 모든 조건은 1회씩 실측한 것이므로, 절감률을 일반화하지 않습니다.
AI 판정 처리 방법
같은 요구사항을 4번 모두 문제 삼았음에도 불구하고, 판정은 부적합, 의문, 부분적으로 흔들렸습니다. 이 1개 요구사항에 한해서는 같은 곳을 찾는 것은 안정적이었으나, 적합 판정은 안정적이지 않았습니다.
그리고 AI가 부적합으로 판정한 것과, 우리가 그 지적을 어떻게 처리할지는 별개의 문제였습니다.
놓친 2건은 결국 고치지 않고 수용했습니다. 구현 방법과 기존 인증 기반에 미치는 영향을 검토한 결과, 단순히 고치면 다른 이용 경로에 영향을 주는 형태였습니다. 검토한 안과 기각 이유, 수용 이유, 재판단할 조건은 내부 기록으로 남겼습니다.
그 과정에서 이전에 내린 판단 이유가 적절하지 않았다는 것도 깨달았습니다. 다른 요구사항을 '만들지 않는다'고 결정했을 때의 이유는 브랜드 방침에 근거했으나, 실제로는 기술적 제약과 비용 대비 효과에 관한 것이었습니다. 이유를 다시 작성함으로써, 다음에 검토할 조건도 쓸 수 있게 되었습니다.
수용했다고 해서 요구사항을 충족한 것은 아닙니다. 판정은 부적합 상태로 남겨두고, 그 옆에 수용 판단과 이유를 적습니다. 섞어 쓰면 다음번에 무엇이 미달인지 알 수 없게 됩니다.
같은 이유로 기록의 기호 사용법에도 지적이 나왔습니다. 이쪽에서는 '위임 / 부분 / 판단 필요'를 하나의 기호로 묶었지만, '구현 없음'과 '외부 서비스에 위임하여 확인 불가'는 성질이 다릅니다. 전자는 구현, 후자는 설정 감사, 수용이라면 재판단으로 필요한 대응이 다릅니다.
다음 점검에 필요한 것들
4번을 해보면서 다음에 필요하다고 알게 된 것을 나열합니다.
- 지난 판정과 그 근거의 위치, 확인한 날짜. 날짜가 없으면 이월과 재확인이 구별되지 않습니다. -
- 대상 커밋과 차이점(diff). Bash를 금지하고 실행하려면 외부에서 생성하여 전달해야 합니다. -
- 조사하는 데 필요한 의존 코드와 그 버전. 버전은 우리가 보증합니다. -
- 보지 못한 요구사항을 PASS로 올리지 마십시오. '미재검증(언제 시점)'이라고 적으십시오. -
- 판정이 바뀌면 이유를 남기십시오. 등급은 흔들리므로, 덮어쓰면 이력이 사용할 수 없게 됩니다. 판정과 수용 판단을 분리하십시오
맺음말
Skill은 요구사항 원문과 점검 절차를 가지고 있습니다. 반면에 지난 기록, 변경의 차이점(diff), 의존 코드와 그 버전을 갖추는 것은 우리의 작업이었습니다. 이것들을 갖추지 않으면, 점검 시간이 짧아져도 놓치게 됩니다.
아직 측정하지 못한 것이 2가지 있습니다. 기능 추가만의 차이점으로 재확인 건수 자체를 줄일 수 있는지 여부입니다. 이번의 차이점은 시정 작업 그 자체였기 때문에, 절약이 가장 나오기 어려운 조건이었습니다. 또 하나는 변동성인데, 4개 조건을 각각 한 번씩만 실행했습니다.
agent-asvs
에는 CI용 변종도 준비되어 있습니다. 앞으로는 CI에 통합하는 것도 시도하고 싶습니다. 다만, 이번의 차이점 재점검을 자동화하려면, 지난 판정 기록이나 차이점, 의존 코드를 갖추는 메커니즘도 필요하게 됩니다. 우선은 손으로 실행할 수 있는 워크플로우부터 시도할 생각입니다.
전작에서는 '다음에는 차이점을 보면 될 것'이라고 썼지만, 차이점만으로는 부족했습니다. 의존 코드와 그 버전까지 갖추어 전달할 필요가 있었습니다. 다음에 한 바퀴 돌 때는 거기서 시작하겠습니다.
Discussion