
보안 연구 랩: CVE-2025-61584 (GHSA-9g7x-737f-5xpc) 재현 — pull_request_target 워크플로우(.github/workflows/pr.yml)에서 github.head_ref를 통한 명령어 주입
자동화된 연구 산출물 — 업스트림 프로젝트가 아님.
이 저장소는 Université Laval의 석사 논문을 위해 게시된 GitHub Actions 워크플로우 취약점 재현을 목적으로 자동화된 하네스가 구축한 일회용 실험실입니다. 이는 커밋
b0b1a1538aeb1991b5bc13dfe4e18e686913b12e(2025-04-26) 시점의serverless-dns/serverless-dns를 그대로 스냅샷한 것이며, 해당 프로젝트의 자체 라이선스에 따라 재배포되며, 해당 라이선스 파일은 이 스냅샷에 변경 없이 포함되어 있습니다.업스트림 프로젝트는 이와 관여하지 않으며, 결코 공격 대상이 아닙니다. 여기서 연구된 취약점은 이미 공개된 것입니다. 이 저장소의 모든 시크릿과 변수는 무작위로 생성된 더미 값입니다 — 실제 자격 증명은 존재하지 않습니다. 액션 참조와 러너 이미지는 2025-04-26에 확인된 값으로 고정되어 있습니다. 스냅샷에 적용된 모든 변경 사항은 하네스 출력의
pinning.md를 참조하세요.질문 또는 이의 제기: [email protected]
_serverless-dns_는 Pi-Hole 유사 콘텐츠 차단, 서버리스, 스텁 DNS-over-HTTPS(DoH) 및 DNS-over-TLS(DoT) 리졸버입니다. Cloudflare Workers, Deno Deploy, Fastly Compute@Edge, Fly.io에서 즉시 실행됩니다. 이러한 모든 서비스의 무료 티어는 월간 10~20개 기기의 DNS 트래픽을 충당하기에 충분합니다.
RethinkDNS는 다음 엔드포인트에서 프로덕션 환경에 serverless-dns를 실행합니다:
서버 측 처리 시간은 0밀리초(ms)에서 2ms(중앙값)이며, 종단 간 지연 시간(지역 및 네트워크에 따라 다름)은 10ms에서 30ms(중앙값)입니다.
Fly.io의 Rethink DNS 리졸버는 FOSS United의 후원을 받습니다.
Cloudflare Workers는 serverless-dns를 설정하기 가장 쉬운 플랫폼입니다:
단계별 지침은 다음을 참조하세요:
| 플랫폼 | 난이도 | 런타임 | 문서 |
|---|---|---|---|
| ⛅ Cloudflare | 쉬움 | v8 Isolates | Cloudflare Workers에서 호스팅 |
| 🦕 Deno.com | 보통 | Deno Isolates | Deno.com에서 호스팅 |
| ⏱️ Fastly Compute@Edge | 쉬움 | Fastly JS | Fastly Compute@Edge에서 호스팅 |
| 🪂 Fly.io | 어려움 | Node MicroVM | Fly.io에서 호스팅 |
블록리스트를 설정하려면 브라우저에서 https://<my-domain>.tld/configure를 방문하세요(RethinkDNS의 configure 페이지와 유사한 화면이 로드되어야 합니다).
코드:
# 작업 디렉토리로 이동
cd /my/work/dir
# 이 저장소 클론
git clone https://github.com/serverless-dns/serverless-dns.git
# serverless-dns로 이동
cd ./serverless-dns
Node:
# 필요한 경우 nvm을 통해 node v22+ 설치
# https://github.com/nvm-sh/nvm#installing-and-updating
wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
nvm install --lts
# 의존성 다운로드
npm i
# (선택 사항) 의존성 업데이트
npm update
# node에서 serverless-dns 실행
./run n
# clinicjs.org 프로파일러 실행
./run n [cpu|fn|mem]
Deno:
# deno.land v2+ 설치
# https://github.com/denoland/deno/#install
curl -fsSL https://deno.land/install.sh | sh
# deno에서 serverless-dns 실행
./run d
Fastly:
# 필요한 경우 nvm을 통해 node v22+ 설치
# Fastly CLI 설치
# https://developer.fastly.com/learning/tools/cli
# Fastly Compute@Edge에서 serverless-dns 실행
./run f
Wrangler:
# Cloudflare Workers (cli) aka Wrangler 설치
# https://developers.cloudflare.com/workers/cli-wrangler/install-update
npm i wrangler --save-dev
# Cloudflare Workers (cli)에서 serverless-dns 실행
# 먼저 Wrangler를 설정해야 합니다:
# https://developers.cloudflare.com/workers/cli-wrangler/authentication
./run w
# Chrome DevTools로 wrangler 프로파일링
# blog.cloudflare.com/profiling-your-workers-with-wrangler
이 저장소의 커밋은 Google JavaScript 스타일 가이드(.eslintrc.cjs 참조)를 준수합니다.
.js 파일에 린터(eslint)와 포맷터(prettier)를 실행하는 git pre-commit 훅이 있습니다. 이 훅을 우회하려면 git commit --no-verify를 사용하세요.
풀 리퀘스트도 코드 스타일 위반 여부를 확인하며 가능한 경우 자동으로 수정됩니다.
기본값을 조정해야 하는 경우 env.js를 구성하세요.
Cloudflare Workers의 경우 wrangler.toml에서 환경 변수를 설정하세요.
Fastly Compute@Edge의 경우 fastly.toml에서 환경 변수를 설정하세요.
serverless-dns는 DoH와 DoT 모두에 대해 영숫자 베어러 토큰 인증을 지원합니다. 토큰의 경우 msg-key(시크릿)를 사용하여 hex(hmac-sha256(msg-key|domain.tld), msg)의 출력을 ACCESS_KEYS 환경 변수에 csv 형식으로 추가하세요. 참고: msg는 현재 sdns-public-auth-info로 고정되어 있습니다.
msg-key를 블록스탬프 끝에 다음과 같이 배치합니다:
1:1:4AIggAABEGAgAA:<msg-key> (여기서 1은 버전, 1:4AIggAABEGAgAA
는 블록스탬프, <msg-key>는 인증 시크릿, :는 구분자입니다).msg-key를 블록스탬프가 포함된 SNI(도메인 이름) 끝에 배치합니다:
1-4abcbaaaaeigaiaa-<msg-key> (여기서 1은 버전, 4abcbaaaaeigaiaa
는 블록스탬프, <msg-key>는 인증 시크릿, -는 구분자입니다).DoT에서도 인증을 사용하려면 서브도메인은 총 63자로 제한되므로 msg-key를 더 짧게(8~24자) 유지하세요.
다음과 같이 max.rethinkdns.com에서 포크에 대한 액세스 키를 생성할 수 있습니다:
msgkey="ShortAlphanumericSecret"
domain="my-serverless-dns-domain.tld"
curl 'https://max.rethinkdns.com/genaccesskey?key='"$msgkey"'&dom='"$domain"
# 출력
# {"accesskey":["my-serverless-dns-domain.tld|deadbeefd3adb33fa2bb33fd3eadf084beef3b152beefdead49bbb2b33fdead83d3adbeefdeadb33f"],"context":"sdns-public-auth-info"}
serverless-dns는 Cloudflare Logpush를 통해 로그를 업로드하도록 설정할 수 있습니다.
CF_ACCOUNT_ID=<hex-cloudflare-account-id>
CF_API_KEY=<계정 수준에서 logs-edit 권한이 있는 api-key>
R2_BUCKET=<r2-bucket-name>
R2_ACCESS_KEY=<버킷에 대한 r2-access-key>
R2_SECRET_KEY=<읽기-쓰기 권한이 있는 r2-secret-key>
# 선택 사항: 이 워커의 로그만 푸시되도록 필터를 설정합니다. 그러나
# Worker 이름(script-name)에 대한 필터가 필요하지 않은 경우 아래 "filter" 필드를 그에 맞게 편집하세요.
SCRIPT_NAME=<wrangler-toml에 정의된 워커 이름>
# 더 많은 옵션은 developers.cloudflare.com/logs/get-started/api-configuration 참조
# cURL을 사용한 Logpush API: developers.cloudflare.com/logs/tutorials/examples/example-logpush-curl
# 사용 가능한 Logpull 필드: developers.cloudflare.com/logs/reference/log-fields/account/workers_trace_events
curl -s -X POST "https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/logpush/jobs" \
-H "Authorization: Bearer ${CF_API_KEY}" \
-H 'Content-Type: application/json' \
-d '{
"name": "dns-logpush",
"logpull_options": "fields=EventTimestampMs,Outcome,Logs,ScriptName×tamps=rfc3339",
"destination_conf": "r2://'"$R2_BUCKET"'/{DATE}?access-key-id='"${R2_ACCESS_KEY}"'&secret-access-key='"${R2_SECRET_KEY}"'&account-id='"{$CF_ACCOUNT_ID}"',
"dataset": "workers_trace_events",
"filter": "{\"where\":{\"and\":[{\"key\":\"ScriptName\",\"operator\":\"contains\",\"value\":\"'"${SCRIPT_NAME}"'\"},{\"key\":\"Outcome\",\"operator\":\"eq\",\"value\":\"ok\"}]}}",
"enabled": true,
"frequency": "low"
}'
wrangler.toml 속성 logpush = true를 설정하면 Logpush가 활성화됩니다.LOG_LEVEL = "logpush"는 요청 및 오류 로그만 출력되도록 로그 수준을 높입니다.LOGPUSH_SRC = "csv,of,subdomains"를 설정하면 Workers hostname이 서브도메인 중 하나를 포함하는 경우에만 log-pusher.js가 요청 로그를 출력합니다.R2에 게시된 로그는 R2 Workers, R2 API 또는 Logpush API를 사용하여 검색할 수 있습니다.
Workers Analytics는 활성화된 경우 로그 키인 lid에 대해 푸시되며, 지정하지 않으면 서버리스 배포의 호스트 이름에서 마침표 .를 밑줄 _로 바꾼 값으로 설정됩니다. API를 통해 Analytics를 쿼리할 때는 인증이 필요하며 json을 반환합니다. 예: https://max.rethinkdns.com/1:<optional-stamp>:<msg-key>/analytics?t=<time-interval-in-mins>&f=<field-name>. 가능한 fields는 ip(클라이언트 ip), qname(dns 쿼리 이름), region(리졸버 지역), qtype(dns 쿼리 유형), dom(최상위 도메인), ansip(dns 응답 ips), cc(ans ip 국가 코드)입니다.
로그 캡처 및 분석은 Fly 및 Deno Deploy에서는 아직 구현되지 않았습니다.
Deno Deploy(클라우드)와 Deno(런타임)는 동일한 API 표면을 노출하지 않습니다(예: Deno Deploy는 HTTP/S 서버 리스너만 지원하는 반면, Deno는 일반 HTTP 및 HTTP/S 외에도 원시 TCP/UDP/TLS를 지원합니다).
Node를 제외하고 serverless-dns는 환경 변수 CF_DNS_RESOLVER_URL / CF_DNS_RESOLVER_URL_2로 정의된 DoH 업스트림을 사용합니다. Node에서 기본 DNS 업스트림은 1.1.1.2(참조) 또는 Fly.io에서 실행 시 fdaa::3의 재귀 DNS 리졸버입니다.
Node 및 Deno의 진입점은 각각 src/server-node.js, src/server-deno.ts이며, 둘 다 TCP-over-TLS, HTTP/S 연결을 수신합니다. 반면 HTTP(cli) 또는 HTTP/S(프로덕션)만 수신하는 Cloudflare Workers의 진입점은 src/server-workers.js이고, Fastly의 경우 src/server-fastly.js입니다.
Node의 로컬(비프로덕션) 설정에서 key(개인) 및 cert(공개 체인) 파일은 기본적으로 환경 변수 TLS_KEY_PATH 및 TLS_CRT_PATH에 정의된 경로에서 읽습니다.
Node의 프로덕션 설정(Fly.io)의 경우 TLS_OFFLOAD를 true로 설정하거나 key 및 cert가 환경 변수 TLS_CERTKEY에 base64 로 인코딩되어야 합니다(참조). 예:
# 또는: tls를 fly.io로 오프로드하고 tls_offload를 true로 설정
TLS_OFFLOAD="true"
# 또는: key(개인)와 cert(공개 체인)의 base64 표현
TLS_CERTKEY="KEY=b64_key_content\nCRT=b64_cert_content"
Deno의 경우 key 및 cert 파일은 환경 변수 TLS_KEY_PATH 및 TLS_CRT_PATH(참조)에 정의된 경로에서 읽습니다.
프로세스 기동은 각 런타임마다 다릅니다: Node의 경우 src/core/node/config.js가 기동 을 관장하며, Deno의 경우 src/core/deno/config.ts, Workers의 경우 src/core/workers/config.js입니다. src/system.js pub-sub은 다양한 모듈 간의 기동 단계를 조정합니다.
Node 및 Deno에서 프로세스 내 DNS 캐싱은 @serverless-dns/lfu-cache로 지원됩니다. Cloudflare Workers는 Cache Web API와 프로세스 내 lfu 캐시로 지원됩니다. 세 플랫폼 모두에서 캐싱을 완전히 비활성화하려면 환경 변수 PROFILE_DNS_RESOLVES=true를 설정하세요.
Cloudflare Workers와 Deno Deploy는 일시적입니다. 즉, 클라이언트 요청을 처리하는 "프로세스"가 오래 지속되지 않으며, 실제로 연속된 두 요청이 서로 다른 isolates("프로세스")에서 처리될 수 있습니다. Fastly Compute@Edge도 일시적이지만 isolates를 사용하지 않고, 대신 Fastly는 각 요청에 대해 wasmtime 샌드박스를 생성하고 파괴합니다. Node를 실행하는 Fly.io의 리졸버는 영구 VM으로 지원되므로 기존의 "serverfull" 환경처럼 더 오래 지속됩니다.
Deno Deploy의 경우 코드베이스는 deno bundle로 단일 javascript 파일로 번들된 후 Deno.com에 전달됩니다.
Cloudflare Workers의 빌드 타임 및 런타임 구성은 wrangler.toml에 정의되어 있습니다. Webpack5는 파일을 ESM 모듈로 번들한 다음 Wrangler 가 Cloudflare에 업로드합니다.
Fastly Compute@Edge의 빌드 타임 및 런타임 구성은 fastly.toml에 정의되어 있습니다. Webpack5는 파일을 ESM 모듈로 번들한 다음 npx js-compute-runtime으로 WASM으로 컴파일되고 이후 Fastly CLI 로 패키징 및 게시되어 Fastly Compute@Edge에 배포됩니다.
Node를 실행하는 Fly.io의 경우 런타임 지시문은 fly.toml(dev 및 live 배포 유형에 사용)에 정의되어 있고, 배포 지시문은 node.Dockerfile에 있습니다. flyctl은 이에 따라 Fly.io 인프라에 serverless-dns를 설정합니다.
# cloudflare workers.dev용 빌드 및 배포
npm run build
# 일반적으로 env-name은 prod입니다
npx wrangler publish [-e <env-name>]
# fastly compute@edge용 번들, 빌드 및 배포
# developer.fastly.com/reference/cli/compute/publish
fastly compute publish
# fly.io용 빌드 및 배포
npm run build:fly
flyctl deploy --dockerfile node.Dockerfile --config <fly.toml> [-a <app-name>] [--image-label <some-uniq-label>]
Fly.io에 TLS 종료를 오프로드하는 배포(B1 배포 유형)의 경우 런타임 지시문은 대신 fly.tls.toml에 정의되어 있으며, 포트 443에서 HTTP2 Cleartext 및 HTTP/1.1을 설정하고 포트 853에서 TCP를 통한 DNS를 설정합니다.
참조: github/workflows.
190개 이상의 블록리스트는 Succinct Radix Trie(Steve Hanov의 구현 기반)로 압축되며, "간결성"을 희생하여 문자열 검색(lookup) 속도를 높이기 위한 수정이 적용되었습니다. 블록리스트는 unix 타임스탬프(src/basicconfig.json에 정의, pre.sh가 다운로드)로 버전 관리되며, 일주일에 한 번 생성되지만 가능하면 매일/매시간 생성하고 싶습니다(참조). Cloudflare R2(환경 변수: CF_BLOCKLIST_URL)에서 호스팅됩니다.
serverless-dns는 런타임 기동 중 radix-trie 설정에 필요한 3개의 블록리스트 파일을 다운로드하거나, DNS 요청을 처리할 때 지연 다운로드합니다.
serverless-dns는 190개 이상의 블록리스트에서 약 1,300만 개의 항목(2023년 1월 기준)을 컴파일합니다. 이는 serverless-dns/blocklists 저장소에 정의되어 있습니다.