
Consul Template은 템플릿 평가 중 심볼릭 링크가 가리키는 위치를 검증했지만, 이후 의존성 페치(fetch)에서는 원래 경로를 읽었습니다. 두 작업 사이에 링크를 재지정함으로써 샌드박스 내부 파일 참조가 샌드박스 외부 파일 노출로 바뀌었습니다.
Consul Template는 템플릿 평가 중 심볼릭 링크가 가리키는 위치를 검증했지만, 이후 의존성 페치는 원래 경로를 읽었습니다. 두 작업 사이에 링크를 다시 지정하면 샌드박스 내부 파일 참조가 샌드박스 외부 파일 공개로 바뀌었습니다.
저는 HashiCorp Consul Template를 검토하면서 아주 구체적인 보안 질문 하나를 염두에 두고 이 문제를 발견했습니다:
sandbox_path가 템플릿 평가 중 심볼릭 링크를 검증한다면, 이후의 파일 읽기는 그 검증된 동일한 대상에 바인딩된 상태로 유지될까요?
이 경우 답은 '아니요'였습니다.
file 템플릿 헬퍼는 경로를 해석하고 템플릿 평가 중 설정된 샌드박스를 적용했습니다. 하지만 그 검사 이후에는 원래의 원시 경로를 사용하여 파일 의존성을 생성했습니다.
그 의존성은 나중에 페치되었습니다.
공격자가 검증과 의존성 페치 사이의 틈에서 심볼릭 링크를 다시 지정하면, Consul Template는 샌드박스 외부 파일을 읽을 수 있었습니다. 링크가 다음 렌더링 전에 복원되면 샌드박스 검증이 다시 통과했고 캐시된 외부 콘텐츠가 여전히 렌더링되었습니다.
그 문제가 CVE-2026-5061이 되었습니다.
HashiCorp 공지: HCSEC-2026-12
IBM 공지: CVE-2026-5061 security bulletin
CVE: CVE-2026-5061
수정 버전: 0.42.0
photo0
attacker-controlled in-sandbox symlink -> sandbox validation resolves to safe target -> dependency stores original raw path -> attacker retargets link outside sandbox -> dependency fetch reads external file -> attacker restores safe link -> next validation passes -> cached external content is rendered
Consul Template는 Consul 및 Vault 데이터를 위한 템플릿 렌더링 도구입니다.
지속적으로 실행되고, 의존성의 변경을 감시하며, 데이터를 애플리케이션이 사용할 수 있도록 파일이나 환경 변수로 렌더링할 수 있습니다.
file 템플릿 헬퍼는 로컬 파일을 읽고 그 내용을 렌더링된 출력에 삽입합니다.
로컬 파일 읽기는 프로세스가 읽을 수 있는 비밀을 노출할 수 있기 때문에, Consul Template는 이 헬퍼의 경계로 sandbox_path를 제공합니다.
문서화된 보안 속성은 명확합니다:
file에 전달되는 경로는 설정된 샌드박스 안에 있어야 합니다이로써 sandbox_path는 실제 보안 경계가 됩니다.
중요한 질문은 경로가 샌드박스 안에 있는 것처럼 보이는지가 아니었습니다.
진짜 질문은 다음과 같았습니다:
읽히는 파일이 샌드박스 검증을 통과한 바로 그 파일로 유지되는가?
취약한 버전에서는 그렇지 않았습니다.
파일시스템 샌드박스는 경로 검증과 파일 접근 사이의 틈에서 자주 실패합니다.
일반적인 패턴은 다음과 같습니다:
공격자가 두 작업 사이에 심볼릭 링크 또는 이에 상응하는 파일시스템 리디렉션을 수정할 수 있다면 그 가정은 안전하지 않습니다.
Consul Template는 템플릿 평가와 의존성 페치가 별도의 단계였기 때문에 이 공격 표면을 특히 흥미롭게 만들었습니다.
그 분리는 올바른 보안 질문을 만들어냈습니다:
의존성 페치는 검증된 대상에 바인딩되어 있는가, 아니면 공격자가 제어하는 경로를 다시 해석하는가?
그것이 제가 집중한 경계였습니다.
근본 원인은 다음 사이의 TOCTOU(time-of-check to time-of-use) 불일치였습니다:
template/funcs.go의 샌드박스 검증dependency/file.go의 이후 의존성 읽기테스트한 리비전에서 fileFunc()는 다음과 같이 동작했습니다:
normalized := strings.TrimSpace(s)
err := pathInSandbox(sandboxPath, normalized)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(s)
검증 헬퍼는 포함 여부를 확인하기 전에 심볼릭 링크를 올바르게 해석했습니다:
sandboxResolved, err := filepath.EvalSymlinks(filepath.Clean(sandbox))
targetResolved, err := filepath.EvalSymlinks(filepath.Clean(path))
rel, err := filepath.Rel(sandboxResolved, targetResolved)
if rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("'%s' is outside of sandbox", path)
}
따라서 샌드박스 검사 자체는 해석된 대상을 이해하고 있었습니다.
하지만 그 해석된 대상은 버려졌습니다.
NewFileQuery()는 대신 원래 문자열을 저장했습니다:
return &FileQuery{
stopCh: make(chan struct{}, 1),
path: s,
}, nil
그런 다음 이후의 의존성 페치가 그 경로를 다시 읽었습니다:
data, err := os.ReadFile(d.path)
그것이 전체 취약점입니다.
코드는 하나의 파일시스템 해석을 검사한 뒤, 나중에 다른 해석을 사용했습니다.
심볼릭 링크는 변경 가능하기 때문입니다.
공격자는 샌드박스 외부 대상을 pathInSandbox()를 직접 통과시킬 필요가 없습니다.
검증 이후 Fetch()가 읽기를 수행하기 전에, 이미 승인된 원시 경로가 해석되는 대상을 바꾸기만 하면 됩니다.
절차는 다음과 같습니다:
pathInSandbox() 중 링크가 안전한 파일로 해석됩니다FileQuery가 원시 링크 경로를 기록합니다os.ReadFile(d.path)가 링크를 다시 해석합니다마지막 단계가 중요합니다.
안전한 링크를 복원해도 이미 페치된 비밀은 의존성 캐시에서 제거되지 않았습니다.
중요한 차이는 명시적 보안 통제의 우회입니다.
이것은 단순히 다음과 같은 것이 아니었습니다:
"Consul Template가 파일을 감시하는 동안 파일이 변경되었다"
파일 변경을 감시하는 것은 예상된 동작입니다.
실제 문제는 다음과 같습니다:
문서화된 샌드박스 제한을 통과한 경로가 나중에 그 샌드박스 외부의 파일을 읽는 데 사용될 수 있다
이것은 직접적인 신뢰 경계 실패입니다.
애플리케이션은 이미 보안 결정을 내렸습니다:
하지만 이후의 페치는 그 결정을 정당화한 대상에 연결되어 있지 않았습니다.
그것이 평범한 파일시스템 가변성을 취약점으로 바꾼 것입니다.
저는 정확한 평가 및 의존성 페치 시퀀스를 중심으로 독립형 재현기를 구축했습니다.
통제된 구성은 다음을 사용했습니다:
재현 흐름은 다음과 같습니다:
file 헬퍼를 평가하여 샌드박스 검증이 성공하고 원시 경로가 의존성으로 등록되게 합니다.os.ReadFile(d.path)가 리디렉션된 링크를 통해 읽게 합니다.관찰된 동작은 다음과 같습니다:
그 해시 비교가 중요했습니다.
최종 렌더링 값이 오래된 안전 콘텐츠, 파일 이름 아티팩트, 또는 오류 경로 부작용이 아님을 증명했습니다.
그것은 샌드박스 외부 파일의 바이트 단위 정확한 내용이었습니다.
TOCTOU 문제에 대한 가장 강력한 증명은 타임라인을 통제해야 합니다.
심볼릭 링크가 샌드박스 외부를 가리킬 수 있다는 것을 단순히 보여주는 것은 더 약한 증명입니다. pathInSandbox()는 외부 대상을 관찰하면 이미 거부했기 때문입니다.
보안 주장은 다음 상태를 순서대로 모두 증명하는 데 의존했습니다:
그래서 재현기는 템플릿 검증, 의존성 페치, 링크 복원, 그리고 다음 렌더링을 명시적으로 분리했습니다.
PoC는 무작위 타이밍이나 반복적인 추측에 의존하지 않았습니다.
취약한 수명 주기를 결정적으로 구동했습니다.
그 덕분에 근본 원인과 영향을 훨씬 더 쉽게 입증할 수 있었습니다.
이 문제는 로컬 파일시스템에 대한 영향력을 요구합니다.
공격자는 취약한 시간 창 동안 관련 심볼릭 링크 또는 이에 상응하는 연결 경로를 생성, 교체, 또는 다시 지정할 수 있는 충분한 접근 권한이 필요합니다.
Consul Template 프로세스는 또한 다음을 충족해야 합니다:
이러한 요구 사항은 중요합니다.
이것은 기본 배포에서 인증 없이 원격으로 임의 파일을 읽을 수 있는 취약점이 아니었습니다.
하지만 영향을 받는 로컬 신뢰 모델 내에서 그 영향은 실질적이었습니다:
sandbox_path 제한 우회프로세스가 민감한 자격 증명, 서비스 구성, 토큰 또는 개인 키에 접근할 수 있는 상태로 실행되면 위험이 증가합니다.
HashiCorp는 이 문제를 다음과 같이 분류했습니다:
CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
게시된 기본 점수는 다음과 같습니다:
4.7 / Medium
이 점수는 실제 제약 조건을 반영합니다:
이는 합리적인 분류입니다.
이 문제는 공격자 전제 조건이 좁지만, 샌드박스 우회와 그로 인한 파일 공개는 모두 구체적입니다.
HashiCorp 공지에는 다음과 같이 나열되어 있습니다:
Affected: consul-template up to 0.41.4
Fixed: consul-template 0.42.0
수정은 2026년 4월 15일 0.42.0 릴리스에 포함되었습니다.
수정은 작았고 깨진 바인딩을 직접 해결했습니다.
패치된 코드는 해석된 대상을 검증한 다음 원시 입력에서 의존성을 만드는 대신, 해석된 경로를 반환하고 그 경로를 NewFileQuery()에 전달합니다:
resolvedPath, err := resolveSandboxedPath(
sandboxPath,
strings.TrimSpace(s),
)
if err != nil {
return "", err
}
d, err := dep.NewFileQuery(resolvedPath)
보안 속성은 다음에서:
다음으로 변경되었습니다:
이는 보고된 심볼릭 링크 재지정 경로를 차단합니다. 원래 링크를 변경해도 의존성이 저장한 경로가 더 이상 변경되지 않기 때문입니다.
패치는 또한 정확한 시퀀스를 다루는 집중된 회귀 테스트를 추가했습니다:
이것이 이런 버그에 대해 원하는 유형의 수정입니다:
저는 2026년 3월 20일에 이 문제를 HashiCorp 보안팀에 비공개로 보고했습니다.
보고서에는 다음이 포함되었습니다:
HashiCorp는 0.42.0에서 문제를 수정하고 2026년 5월 12일에 HCSEC-2026-12를 공개했습니다.
IBM은 동일한 CVE에 대한 대응 보안 공지를 게시했습니다.
두 공식 공지 모두 보고자를 다음과 같이 인정했습니다:
Mohamed Abdelaal (0xmrma)
핵심 교훈은 간단합니다:
이후의 파일 작업이 그 경로를 다른 객체로 해석할 수 있다면, 경로를 검증하는 것만으로는 충분하지 않습니다
이 규칙은 Consul Template를 훨씬 넘어 적용됩니다.
다음을 수행하는 모든 코드에서 중요합니다:
더 깊은 보안 속성은 다음과 같은 것이 아닙니다:
"경로 문자열이 한때 안전해 보였다"
그것은 다음과 같습니다:
민감한 작업에 사용되는 객체는 검증을 통과한 객체여야 한다
이 경우 Consul Template는 하나의 해석을 검증하고 다른 해석을 페치했습니다.
그 틈만으로 충분했습니다.
sandbox_path는 명시적인 로컬 파일 보안 경계였습니다pathInSandbox()는 심볼릭 링크 대상을 올바르게 해석하고 검증했습니다FileQuery는 공격자의 영향을 받은 원래 경로를 유지했습니다os.ReadFile을 통해 그 경로를 다시 해석했습니다0.42.0 버전은 의존성을 해석되고 검증된 경로에 바인딩하여 버그를 수정했습니다이 취약점은 문자열 접두사 검사를 우회하는 문제가 아니었습니다.
시간과 정체성에 관한 문제였습니다.
Consul Template는 템플릿 평가 중 링크가 가리키는 위치를 확인했습니다. 이후 의존성 페치는 파일시스템에 같은 질문을 다시 던졌습니다. 공격자는 그 두 순간 사이에 답을 바꿀 수 있었습니다.
그것이 이것이 CVE-2026-5061이 된 이유입니다.
consul-template 0.42.0에서 수정되었습니다.