탐지 모델
Dalfox의 모든 탐지 결과는 서로 다른 세 질문에 답합니다. 이 셋을 하나의 척도로 읽는 것이 출력을 오해하는 가장 큰 원인이라, 질문마다 필드를 따로 둡니다.
| 축 | 필드 | 답하는 질문 |
|---|---|---|
| 신뢰도 | type: V / R |
이것을 취약점이라고 말할 수 있나? |
| 방식 | detection_method |
어떻게 찾았나? |
| 영향도 | severity |
악용되면 얼마나 나쁜가? |
지금 서로 독립적으로 움직이는 것은 앞의 둘뿐입니다. XSS 탐지 결과에서
severity는 티어를 다시 말한 값이라(V → High, A → Medium,
R → Info), 여기에 필터나 정렬을 걸어도 type 이상의 정보는 얻을 수
없습니다. 실제 정보를 담는 건 라이브러리 권고에서 값을 가져오는 I뿐입니다.
severity가 스스로 영향도를 판정하기 전까지는 표시용으로만 보세요.
[A]는 이 분리보다 먼저 생겼습니다. type 필드에 있으면서 방식 질문에 답하기 때문에, 코드를 포함해 누구도 이것이 신뢰도 척도에서 어디에 속하는지 말할 수 없었습니다. 아래 전환 절에 따라 흡수될 예정입니다.
이 페이지는 그 구분이 출력만 봐서는 드러나지 않아서 만들었습니다. 이슈 #1238에서 @OSTARA711과 함께 정리한 내용이 바탕입니다.
신뢰도: type의 의미
| 태그 | 이름 | 의미 |
|---|---|---|
V |
Vulnerable(취약) | Dalfox가 이 입력이 악용 가능하다고 판단합니다. 조치하세요. |
R |
Reflected(반사됨) | 페이로드가 응답에 돌아왔지만 실행 가능한 위치인지는 확인되지 않았습니다. 신호로 보고 직접 확인하세요. |
A |
AST 탐지 | 과도기용 방식 라벨입니다. 전환 참고. |
I |
정보성 | XSS 주장이 아닙니다 (예: 알려진 취약 JS 라이브러리, CWE-1104). |
--only-poc로 필터링합니다 (예: --only-poc v, --only-poc v,a, --only-poc i).
V가 뜻하지 않는 것
V는 브라우저에서 페이로드가 실행됐다는 뜻이 아닙니다. Dalfox는 브라우저를 구동하지 않고 CDP도 쓰지 않습니다. 페이지를 렌더링하거나 alert()가 뜨는 것을 관찰하지도 않습니다. 요청 기반 방식에서 V는 페이로드가 실제 HTTP 응답으로 파싱한 DOM 트리에서 발견됐다는 뜻입니다. 정적 분석이지만 R의 단순 문자열 매칭보다 강한 증거입니다.
Dalfox가 실제 실행을 관측하는 방식은 대역외 콜백(--blind-oob를 쓴 blind XSS) 하나뿐입니다. 주입한 <script src=…>가 콜백을 보내오면, 무언가가 그 페이로드를 불러와 가져간 것입니다. 대부분은 피해자의 브라우저지만, 연관된 DNS나 HTTP 상호작용이면 무엇이든 인정되므로 URL을 따라가는 서버 측 fetcher도 콜백을 일으킵니다. 경험적 증거이고 Dalfox가 만드는 증거 중 가장 강하지만, Dalfox가 제어하지 않는 클라이언트에서 옵니다. 일반 -b 콜백 URL은 호출이 여러분의 리스너로 가므로 Dalfox는 탐지 결과를 기록하지 않습니다.
방식: detection_method의 의미
| 값 | 무엇을 읽는가 | 페이로드 전송? |
|---|---|---|
reflection |
응답 본문에서 페이로드 바이트 | 예 |
dom-verification |
HTML로 파싱한 응답에서 실행 가능한 위치 | 예 |
ast |
응답에 담긴 JavaScript에서 소스→싱크 흐름 | 흐름을 찾는 데는 보내지 않음. 프로브 응답에서 찾은 흐름은 재확인 요청 1건을 보냄 |
oob |
페이로드를 불러온 쪽이 보낸 대역외 콜백(DNS 또는 HTTP) | 예 |
library |
<script> 태그에서 알려진 취약 버전 |
아니오 |
AST 탐지 결과를 고를 때는 type == "A"가 아니라 detection_method == "ast"를 쓰세요. 방식 필드는 안정적이고, 티어는 그렇지 않습니다.
실제로 나오는 조합
티어와 방식은 따로 정해지므로 V가 곧 dom-verification을 뜻하지 않습니다.
Dalfox가 내보내는 탐지 결과는 다음 중 하나입니다.
type |
detection_method |
confidence |
severity |
산출 경로 |
|---|---|---|---|---|
V |
dom-verification |
high |
High | 전용 DOM 검증 요청 |
V |
reflection |
high |
High | 반사 단계에서, 반사 응답 자체가 이미 실행 가능한 위치로 파싱되는 경우 |
V |
oob |
high |
High | 대역외 콜백 |
V |
ast |
high 또는 low |
High | 레거시 AST 승격 두 곳. 전환 절이 다루는 바로 그 불일치 |
A |
ast |
high 또는 low |
Medium | 그 외 모든 소스→싱크 흐름 |
R |
reflection |
low |
Info | 실행 가능 위치가 확인되지 않은 반사. --hpp의 중복 파라미터 반사(inject_type: inHTML-HPP) 포함 |
I |
library |
(없음) | Medium / High | --detect-outdated-libs |
사람들이 의아해하는 건 V + reflection 행입니다. reflection은 증거가 얼마나
약한지가 아니라 어느 요청에서 증거가 나왔는지를 가리킵니다. 이 행도
dom-verification 행과 똑같은 DOM을 파싱했습니다. 다만 요청을 하나 더 보내는 대신
이미 받아 둔 응답을 썼을 뿐입니다.
dom-verification의 증거
페이로드가 실행 가능한 위치에 도달했음을 증명하는 경로는 다섯 가지입니다.
- CSS 셀렉터로 매칭된 Dalfox 마커
- 위험한 속성에 들어간 실행 가능 스킴(
javascript:,data:text/html,vbscript:) - 핸들러(또는
<script>본문)가 싱크를 호출하는 주입된 엘리먼트 - AST 범위가 페이로드를 포함하는
<script>내부 싱크 호출 - 페이로드가 자신을 둘러싼 JS 문자열을 끝내 버린 인라인 핸들러 브레이크아웃
어느 경로로 증명됐는지는 evidence 필드에 나옵니다.
응답 content type이 허용하는 것
증거는 브라우저가 그 응답에 쓸 파서에서 성립해야 합니다. 그래서 어떤 검사가 성공할 수 있는지는 응답의 Content-Type이 정합니다.
| 응답 타입 | 읽는 방식 |
|---|---|
text/html, 또는 쓸 만한 Content-Type이 없고 본문이 HTML처럼 시작하는 경우 |
HTML로 파싱 |
application/xhtml+xml, image/svg+xml, 그 밖의 XML(text/xml, application/xml, *+xml) |
XML로 파싱. 활성 네임스페이스(XHTML, SVG)에 있는 마크업만 인정 |
JavaScript(application/javascript, text/javascript 등) |
스크립트로 읽음. JSONP 콜백 같은 JS 컨텍스트 페이로드만 V가 되며, 본문 속 HTML 마크업은 절대 V가 되지 않음 |
application/json, *+json, text/csv, 명시된 text/plain, 바이너리 미디어 |
마크업 문서가 아님. 반사를 버리며 R도 남기지 않음 |
text/plain은 X-Content-Type-Options: nosniff 유무와 상관없이 무해로 취급합니다. 브라우저는 명시된 text/plain을 HTML로 스니핑하지 않기 때문입니다. 대상 페이지에 대한 AST 패스도 같은 규칙을 따라, 그 페이지가 활성 마크업 문서일 때만 실행됩니다. 표의 마지막 행에는 예외가 하나 있습니다. 리다이렉트의 Location 헤더에 페이로드가 반사되면 본문 타입과 상관없이 R을 기록합니다.
ast와 DOM-XSS의 천장
AST 패스는 응답에 담긴 JavaScript를 파싱해, 위험한 소스(location.hash, location.search, document.referrer, postMessage 등)에서 온 데이터가 새니타이저를 거치지 않고 위험한 싱크(innerHTML, document.write, eval 등)에 도달하는지 추적합니다. 각 <script> 블록을 한 번 읽고 발견한 흐름을 모두 보고하므로, 명령줄에 넘기지 않은 입력도 결과에 나올 수 있습니다. 서버로 전송되지 않는 URL 프래그먼트도 마찬가지입니다. 랜딩 페이지에 대한 패스는 -p로 좁혀지지 않습니다. -p는 어떤 파라미터를 요청할지 정하는 옵션이고, 그 패스는 이미 받아 둔 응답을 읽기 때문입니다.
결함처럼 보이지만 결함이 아닌 결과도 이것으로 설명됩니다. 순수 클라이언트 사이드 DOM-XSS에서는 JavaScript가 런타임에 페이로드를 페이지에 써 넣으므로, 페이로드가 서버 응답에 나타나지 않고 응답을 파싱하는 방식들은 찾을 대상이 없습니다. location.hash → innerHTML만 취약한 정적 페이지라면 --only-poc v가 아무것도 반환하지 않는 것이 정상입니다. 확인하려면 브라우저에서 devtools를 켜고 POC URL을 열어 보세요. Dalfox는 모든 AST 탐지 결과에 완성된 POC URL을 출력합니다. 랜딩 페이지에서 나온 탐지 결과 중 URL에 담을 수 없는 소스(window.name, document.referrer, 쿠키, postMessage 등)에는 [manual POC: …] 재현 힌트를 붙입니다.
| 플래그 | 효과 |
|---|---|
--skip-ast-analysis |
소스→싱크 분석을 끕니다 |
--analyze-external-js |
동일 출처 <script src> 번들도 가져와 분석합니다 |
--skip-mining-dom은 이 패스에 영향을 주지 않습니다. 이 플래그는 <input> 요소의 id/name 속성에서 파라미터 이름을 수집하는 작업만 끕니다. 파라미터와 탐색을 참고하세요.
confidence: 주장의 근거 등급
모든 XSS 탐지 결과에는 high 또는 low인 confidence와, 판단 신호를 나열한 confidence_reason이 붙습니다. 요청 기반 방식은 증거를 그대로 따르고, AST 탐지 결과는 흐름의 형태로 등급을 매깁니다.
high가 되려면 다음을 모두 만족해야 합니다.
- 링크만으로 공격자가 도달할 수 있는 소스.
location.*,document.URL,URLSearchParams는 페이로드를 URL에 직접 싣습니다. 원래라면 공격자가 제어하는 구동 페이지가 필요한 소스(window.name,document.referrer,postMessage, 스토리지,history.state)는low입니다. 다만 페이지가 그 소스를 쿼리 파라미터에서 스스로 채워 넣으면 링크만으로도 구동되므로high가 되고, 근거에non-URL source seeded from a query parameter by the page가 붙습니다. - 페이지 CSP가 실행을 허용하는 페이로드. 인라인 스크립트가 허용되거나, 싱크가 스크립트를 직접 실행해서(
eval,Function,document.write,<script>텍스트) 인라인 핸들러 권한에 의존하지 않는 경우입니다. report-only CSP는 아무것도 강제하지 않으므로 등급을 낮추지 않습니다. - Trusted Types에 가로막히지 않음.
require-trusted-types-for 'script'와 TrustedHTML 계열 싱크가 함께 있으면low입니다.
새니타이저는 등급 신호가 아닙니다. 이미 필터로 작동하기 때문입니다. 분석기가 새니타이저를 taint를 지우는 요소로 처리하므로, 탐지 결과가 나왔다면 경로에 알려진 새니타이저가 없었다는 뜻입니다.
confidence_reason은 두 방향을 섞지 않습니다. high면 판단을 뒷받침한 신호를, low면 가로막은 신호만 나열하므로 성립했던 신호는 보이지 않습니다. 정보용으로만 붙는 근거가 하나 있습니다. flow sits inside a conditional branch는 흐름이 조건 분기 안에 있다는 기록으로, high 등급에만 나타나며 그 자체로 등급을 바꾸지 않습니다.
등급이 보이는 곳과 등급을 읽는 동작
json, jsonl, toml, markdown, sarif는 confidence를
confidence_reason과 함께 담고, plain은 등급만 Confidence: 줄로
보여 줍니다. 등급에 반응하는 동작은 세 가지입니다.
--min-confidence high는low탐지 결과(모든R, 그리고 Dalfox가 확신할 수 없는 AST 흐름)를 가장 먼저 걸러 냅니다. 모든 형식의 출력,--stream-findings,target_summary의 대상별 개수,--baseline,--limit(스캔 중 조기 종료와 표시 개수 모두), 종료 코드가 모두 걸러진 뒤의 집합을 봅니다.I는 XSS 주장이 아니라 등급이 없으므로 항상 남습니다. 기본값low는 아무것도 거르지 않습니다. 옵션을 주면meta.min_confidence가{level, dropped}를 보고하므로, 탐지 결과가 모두 걸러져clean으로 보이는 대상과 실제로 아무것도 찾지 못한 대상을 구분할 수 있습니다. 설정 파일, REST API, MCP에서는 같은 옵션을min_confidence로 씁니다.- AST 중복 제거는 type과 severity가 같을 때 등급으로 순위를 가립니다(
high우선). - 종료 코드도 위 필터를 따릅니다.
low탐지 결과만 있던 실행은--min-confidence high에서0으로 끝납니다.
--only-poc와 --limit-result-type은 여전히 type으로 고르고, SARIF
level도 여전히 severity를 따릅니다(등급은 properties에 있습니다).
전환
티어 자체는 아직 바뀌지 않습니다. 지금 등급 기준으로 거르려면 --min-confidence를 직접 켜세요. 티어가 따라 바뀌기 전에 각 탐지 결과가 어디로 갈지 미리 볼 수 있습니다.
- 현재:
type은 그대로이고,detection_method와confidence를 쓸 수 있습니다. 선택자로서의type == "A"는 권장하지 않으니detection_method == "ast"를 쓰세요. - 다음:
--tier-model confidence를 옵트인으로 제공합니다. - 그 다음: 그것이 기본값이 되고,
--tier-model legacy가 탈출구로 남습니다.A는 사라집니다.high등급인 AST 탐지 결과는V, 나머지는R로 갑니다. 원래R이 맡던 자리입니다.R의 이름도 이때 함께 바뀝니다. 그때부터는 반사 말고 다른 탐지 결과도 담게 되기 때문입니다.
--only-poc a는 계속 동작합니다. 티어가 사라진 뒤에는 detection_method == "ast"를 고릅니다. 플래그 값이 제거되는 일은 없습니다.
과도기에는 type과 confidence가 어긋날 수 있습니다. type=V, confidence=low인 탐지 결과가 그런 예입니다. 레거시 경로 두 곳이 근거가 부족한데도 AST 탐지 결과를 V로 승격시키기 때문입니다. 등급은 Dalfox가 실제로 주장할 수 있는 것을, 티어는 지금까지 출력해 온 것을 보고합니다. 이 불일치는 버그가 아니라 미리보기 신호입니다.
티어가 등급에서 파생되기 시작하면 티어를 읽는 모든 것이 함께 바뀝니다. 같은 --only-poc v가 다른 집합을 고르고, 종료 코드도 다른 집합에서 뒤집히며, 중복 제거의 우선순위도 달라집니다. 모두 의도된 결과입니다.
여러 방식이 섞인 출력 읽기
INF found reflected 0 params
WRN XSS found 0 XSS (+3 A)
[POC][A][GET][DOM-XSS] https://target.app/?q=%3Cimg+src%3Dx+onerror%3D…
├── Issue: DOM-based XSS via URLSearchParams.get(q) to innerHTML (needs runtime confirmation)
├── Payload: q=<img src=x onerror=alert(1) class=dlx1944740c>
└── Confidence: high
found reflected 0 params: reflection 방식이 서버 측 반사를 찾지 못했다는 뜻입니다. 정적 사이트라면 당연한 결과입니다.XSS found 0 XSS: 대표 숫자는V만 셉니다.(+3 A)가 아래에 출력될 나머지 티어를 알려 줍니다.[A]블록은 위 두 줄과 상관없이 ast 방식에서 나옵니다.
DOM-XSS 대상에서 요약 줄만 읽으면 리포트의 탐지 결과를 전부 놓칩니다.
티어 합계가 발견 수와 다른 이유
출력 전에 후처리가 두 번 돌기 때문에, 티어별 개수는 스캔 중 기록된 탐지 결과 수와 같지 않습니다.
- 중복
R정리. 같은 대상에서 같은(param, location, inject_type)에V가 있으면R은 제거됩니다. 둘 다 남기면 같은 입력이 서로 다른 강도로 두 번 나열되기 때문입니다.V와A는 제거되지 않습니다. - AST 중복 제거. 같은 소스→싱크 흐름을 프리플라이트, 프로브, 반사 루프가
각각 찾을 수 있습니다. 지문당 하나만 남으며, 기준은
type,severity,confidence순입니다. type이 먼저이므로low등급인V가high인A보다 앞섭니다. 단,--min-confidence high가 중복 제거 전에 그V를 걸러 냈다면A가 남습니다.
--stream-findings는 스캔 루프의 탐지 결과를 기록되는 즉시 출력합니다. R 정리와,
AST 중복을 가장 강한 것 하나로 줄이는 과정보다 먼저입니다. 그래서 스트림에 보였던 R이 최종 리포트에는 없을 수 있습니다. 랜딩 페이지
AST 패스, 오래된 라이브러리, OOB 콜백에서 나온 결과는 최종 리포트에만 나옵니다.
둘이 다르면 최종 리포트를 믿으세요.
티어 선택: --only-poc와 --limit-result-type
둘 다 티어로 고르지만 하는 일이 다릅니다. --only-poc는 쉼표 목록(v,r,a,i)을, --limit-result-type은 all(기본값), v, r, a, i 중 하나를 받습니다.
| 플래그 | 효과 |
|---|---|
--only-poc |
출력을 필터링합니다. 다른 티어는 버려집니다 |
--limit-result-type |
--limit 카운트 대상만 정합니다. 다른 티어도 그대로 보고되며, 한도만 소모하지 않습니다 |
--limit 2 --limit-result-type v는 "V 두 개가 쌓일 때까지 스캔하고, 그 과정에서
찾은 것은 전부 보여 줘"라는 뜻입니다. 나머지를 숨기려면 --only-poc v를
함께 주세요. --only-poc를 주면 --limit은 보여 줄 티어만 셉니다. 리포트에서
숨겨지는 결과 때문에 스캔이 멈추지는 않습니다.
종료 코드
0은 탐지 결과 없음, 1은 티어와 상관없이 탐지 결과가 하나라도 있음을 뜻합니다(--min-confidence, --only-poc, --baseline
필터, 위 정리를 적용한 뒤 기준). 2는 하드 에러(잘못된 입력, 모든 대상 도달 불가,
--output 기록 실패)이며, 탐지 결과 없이 정상적으로 끝나지 못한 실행도 포함합니다.
기본값 --on-session-loss abort에서 세션이 끊긴 경우, 스캔 워커가 크래시한 경우,
심각한 전송 손실이 여기에 해당합니다. R 하나나 --detect-outdated-libs가 만든 I 하나도
V와 똑같이 1을 냅니다. Dalfox가 악용 가능하다고 판단한 것에만 CI를 실패시키려면
--only-poc v를, high 등급 주장에만 실패시키려면 --min-confidence high를 쓰세요.
전체 규칙은 종료 코드에 있습니다.
목적별 플래그 선택
| 목적 | 플래그 |
|---|---|
| Dalfox가 악용 가능하다고 판단한 것만 보기 | --only-poc v |
| 운영 중인 대상에서 정적 분석 노이즈 억제 | --skip-ast-analysis |
| 이름 수집만 끄고 DOM-XSS 탐지는 유지 | --skip-mining-dom |
| 파라미터 하나만 테스트하되 모든 DOM 싱크는 확인 | -p q ([A]는 -p 범위를 따르지 않습니다) |
| 대량의 AST 탐지 결과 트리아지 | -f json으로 받아 confidence로 정렬한 뒤 confidence_reason 읽기 |
| 악용 가능 판단에만 CI 실패시키기 | --only-poc v (그렇지 않으면 어떤 티어든 1) |
| Dalfox가 확신할 수 없는 결과 제외 | --min-confidence high (I는 유지) |