CUtils::ParseServerTime: int64 overflow on far-future @time= tag
CUtils::ParseServerTime hands an attacker-controlled IRCv3 @time= tag
straight to cctz::parse without any range validation. cctz internally calls chrono::time_point_cast<microseconds> which multiplies the parsed seconds
by 1,000,000. Years past roughly 292,000 (when seconds since the epoch
exceeds INT64_MAX / 1_000_000) cause signed overflow inside libc++.
tp.time_since_epoch() is in seconds; the cast at line 575 (and equivalents
inside cctz when it joins seconds + fractional) does seconds * 1_000_000
in int64.
Reachability
@time tags arrive from the IRC server ZNC connects to and end up parsed
via CMessage::InitTime. A malicious or buggy IRC server can send any year
in the tag value.
Production impact: arm64/x86 just wrap the multiply, so tv_sec becomes
garbage and the message ends up with a wrong timestamp in buffer playback.
No crash, no memory corruption.
UBSan / -ftrapv builds: hard abort. Concretely this fires under CIFuzz on
any PR that touches the time-parsing path.
Could send a PR with the bound check + a regression test.
# `CUtils::ParseServerTime`: int64 overflow on far-future `@time=` tag
`CUtils::ParseServerTime` hands an attacker-controlled IRCv3 `@time=` tag
straight to `cctz::parse` without any range validation. cctz internally calls
`chrono::time_point_cast<microseconds>` which multiplies the parsed seconds
by 1,000,000. Years past roughly 292,000 (when seconds since the epoch
exceeds `INT64_MAX / 1_000_000`) cause signed overflow inside libc++.
```cpp
// src/Utils.cpp
569: timeval CUtils::ParseServerTime(const CString& sTime) {
570: using namespace std::chrono;
571: system_clock::time_point tp;
572: cctz::parse("%Y-%m-%dT%H:%M:%E*SZ", sTime, cctz::utc_time_zone(), &tp);
573: struct timeval tv;
574: memset(&tv, 0, sizeof(tv));
575: microseconds usec = duration_cast<microseconds>(tp.time_since_epoch());
576: tv.tv_sec = usec.count() / 1000000;
577: tv.tv_usec = usec.count() % 1000000;
578: return tv;
579: }
```
`tp.time_since_epoch()` is in seconds; the cast at line 575 (and equivalents
inside cctz when it joins seconds + fractional) does `seconds * 1_000_000`
in `int64`.
## Reachability
`@time` tags arrive from the IRC server ZNC connects to and end up parsed
via `CMessage::InitTime`. A malicious or buggy IRC server can send any year
in the tag value.
Production impact: arm64/x86 just wrap the multiply, so `tv_sec` becomes
garbage and the message ends up with a wrong timestamp in buffer playback.
No crash, no memory corruption.
UBSan / `-ftrapv` builds: hard abort. Concretely this fires under CIFuzz on
any PR that touches the time-parsing path.
## Reproduction
```cpp
// poc_servertime.cpp
#include <cstdio>
#include <znc/Utils.h>
int main() {
timeval tv = CUtils::ParseServerTime("202620260-5-25T08:00:00Z");
fprintf(stderr, "tv_sec=%lld tv_usec=%ld\n",
(long long)tv.tv_sec, (long)tv.tv_usec);
}
```
Build:
```bash
clang++ -std=c++17 -fsanitize=address,undefined -g -O1 \
-I include -I build/include -include znc/zncconfig.h \
poc_servertime.cpp build/src/libznc.a \
-lpthread -ldl -framework CoreFoundation -L/opt/homebrew/lib -largon2 \
-o poc_servertime
UBSAN_OPTIONS=halt_on_error=0:print_stacktrace=1 ./poc_servertime
```
Output:
```
__chrono/duration.h:93:79: runtime error: signed integer overflow:
6394015664380800 * 1000000 cannot be represented in type 'long long'
#0 ... chrono::__duration_cast::operator()
#3 cctz::detail::join_seconds<long long, 1000000>
#4 cctz::parse<...>
#5 CUtils::ParseServerTime Utils.cpp:572
#6 main
tv_sec=-7004529196414 tv_usec=-410752
```
## Fix
Validate the seconds range before the cast (or catch it after). Simplest:
```cpp
timeval CUtils::ParseServerTime(const CString& sTime) {
using namespace std::chrono;
system_clock::time_point tp;
if (!cctz::parse("%Y-%m-%dT%H:%M:%E*SZ", sTime, cctz::utc_time_zone(), &tp))
return {};
auto secs = duration_cast<seconds>(tp.time_since_epoch()).count();
constexpr int64_t kMaxSec = INT64_MAX / 1'000'000;
if (secs > kMaxSec || secs < -kMaxSec) return {};
...
}
```
Could send a PR with the bound check + a regression test.
<!-- github-sync-id:4326793874 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
CUtils::ParseServerTime: int64 overflow on far-future@time=tagCUtils::ParseServerTimehands an attacker-controlled IRCv3@time=tagstraight to
cctz::parsewithout any range validation. cctz internally callschrono::time_point_cast<microseconds>which multiplies the parsed secondsby 1,000,000. Years past roughly 292,000 (when seconds since the epoch
exceeds
INT64_MAX / 1_000_000) cause signed overflow inside libc++.tp.time_since_epoch()is in seconds; the cast at line 575 (and equivalentsinside cctz when it joins seconds + fractional) does
seconds * 1_000_000in
int64.Reachability
@timetags arrive from the IRC server ZNC connects to and end up parsedvia
CMessage::InitTime. A malicious or buggy IRC server can send any yearin the tag value.
Production impact: arm64/x86 just wrap the multiply, so
tv_secbecomesgarbage and the message ends up with a wrong timestamp in buffer playback.
No crash, no memory corruption.
UBSan /
-ftrapvbuilds: hard abort. Concretely this fires under CIFuzz onany PR that touches the time-parsing path.
Reproduction
Build:
Output:
Fix
Validate the seconds range before the cast (or catch it after). Simplest:
Could send a PR with the bound check + a regression test.
Same as #2008 ?
Yes, I duplicatedly created them by mistake... Sorry for that