util: validate Duration/Timestamp in TimeUtil::FromString against spec limits (#27443)
## Summary
- `TimeUtil::FromString` for both `Duration` and `Timestamp` accepts malformed inputs that produce protos failing `IsDurationValid`/`IsTimestampValid`.
- The companion JSON parser (`JsonStringToMessage`) correctly rejects all the same inputs, creating a **cross-API divergence**.
- **8 sub-bug classes** identified (4 Duration, 4 Timestamp):
| Class | Type | Trigger | Effect |
|-------|------|---------|--------|
| A | Duration | Leading whitespace + negative (`" -3.54s"`) | Signs mismatch: seconds<0, nanos>0 |
| B | Duration | Double minus (`"--3.54s"`) | Signs mismatch: seconds>0, nanos<0 |
| C | Duration | >9 fractional digits (`"0.1234567890s"`) | nanos exceeds 999999999 |
| D | Duration | seconds > 315576000000 (`"315576000001s"`) | Out-of-range seconds |
| E | Timestamp | Year 0000 (`"0000-12-31T23:59:59Z"`) | seconds < kTimestampMinSeconds |
| F | Timestamp | Year 10000 (`"10000-01-01T00:00:00Z"`) | seconds > kTimestampMaxSeconds |
| G | Timestamp | 5-digit year (`"99999-01-01T00:00:00Z"`) | Wildly past spec max |
| H | Timestamp | Negative year (`"-0001-01-01T00:00:00Z"`) | Not in RFC3339 spec |
**Fix**: Add `IsDurationValid()` check after parsing in the Duration overload, and `IsTimestampValid()` check in the Timestamp overload. Return false and clear output on failure. Both validation functions already exist in `TimeUtil` and are used by `ABSL_DCHECK` elsewhere.
**Impact**: Systems using Duration/Timestamp for security-relevant time bounds (gRPC deadlines, Envoy timeouts, retry intervals, IAM token TTLs). Class A is most dangerous: `seconds * 1e9 + nanos` gives `-2.46e9` instead of intended `-3.54e9` — a 31% magnitude shift. 17 fuzzer crash inputs collected.
Found by AFL++ property-check fuzzing (`pb_json_wkt_fuzzer` mode 1).
## Test plan
- [ ] Verify `TimeUtil::FromString(" -3.54s", &d)` returns false (class A)
- [ ] Verify `TimeUtil::FromString("--3.54s", &d)` returns false (class B)
- [ ] Verify `TimeUtil::FromString("0.1234567890s", &d)` returns false (class C)
- [ ] Verify `TimeUtil::FromString("315576000001s", &d)` returns false (class D)
- [ ] Verify `TimeUtil::FromString("0000-12-31T23:59:59Z", &t)` returns false (class E)
- [ ] Verify `TimeUtil::FromString("10000-01-01T00:00:00Z", &t)` returns false (class F)
- [ ] Verify valid inputs still parse successfully
- [ ] Run existing TimeUtil tests to confirm no regressions
Closes #27443
COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/27443 from jortles:fix/timeutil-fromstring-validation b57f4459f9672337fbd1b6ee77c299c4aebaa33f
PiperOrigin-RevId: 938703001
diff --git a/src/google/protobuf/util/time_util.cc b/src/google/protobuf/util/time_util.cc
index 8cbfe00..d615545 100644
--- a/src/google/protobuf/util/time_util.cc
+++ b/src/google/protobuf/util/time_util.cc
@@ -136,9 +136,9 @@
if (!absl::ParseTime(absl::RFC3339_full, value, &result, nullptr)) {
return false;
}
- timespec spec = absl::ToTimespec(result);
- *seconds = spec.tv_sec;
- *nanos = spec.tv_nsec;
+ *seconds = absl::ToUnixSeconds(result);
+ absl::Duration remainder = result - absl::FromUnixSeconds(*seconds);
+ *nanos = static_cast<int32_t>(absl::ToInt64Nanoseconds(remainder));
return true;
}
@@ -189,7 +189,15 @@
if (!ParseTime(value, &seconds, &nanos)) {
return false;
}
+ // Validate before CreateNormalizedTimestamp, which has a DCHECK on range.
+ if (seconds < kTimestampMinSeconds || seconds > kTimestampMaxSeconds) {
+ return false;
+ }
*timestamp = CreateNormalizedTimestamp(seconds, nanos);
+ if (!IsTimestampValid(*timestamp)) {
+ timestamp->Clear();
+ return false;
+ }
return true;
}
@@ -264,6 +272,10 @@
}
duration->set_seconds(seconds);
duration->set_nanos(static_cast<int32_t>(nanos));
+ if (!IsDurationValid(*duration)) {
+ duration->Clear();
+ return false;
+ }
return true;
}
diff --git a/src/google/protobuf/util/time_util_test.cc b/src/google/protobuf/util/time_util_test.cc
index 7a9c961..0b3528d 100644
--- a/src/google/protobuf/util/time_util_test.cc
+++ b/src/google/protobuf/util/time_util_test.cc
@@ -509,6 +509,33 @@
#endif // !NDEBUG
#endif // GTEST_HAS_DEATH_TEST
+TEST(TimeUtilTest, FromStringRejectsMalformedDuration) {
+ Duration d;
+ // Class A: leading whitespace + negative — signs mismatch
+ EXPECT_FALSE(TimeUtil::FromString(" -3.54s", &d));
+ // Class B: double minus — signs mismatch
+ EXPECT_FALSE(TimeUtil::FromString("--3.54s", &d));
+ // Class C: >9 fractional digits — nanos exceeds 999999999
+ EXPECT_FALSE(TimeUtil::FromString("0.1234567890s", &d));
+ // Class D: seconds > 315576000000 — out of range
+ EXPECT_FALSE(TimeUtil::FromString("315576000001s", &d));
+ // Valid inputs still work
+ EXPECT_TRUE(TimeUtil::FromString("3.54s", &d));
+ EXPECT_TRUE(TimeUtil::FromString("-3.54s", &d));
+ EXPECT_TRUE(TimeUtil::FromString("315576000000s", &d));
+}
+
+TEST(TimeUtilTest, FromStringRejectsMalformedTimestamp) {
+ Timestamp t;
+ // Class E: year 0000
+ EXPECT_FALSE(TimeUtil::FromString("0000-12-31T23:59:59Z", &t));
+ // Class F: year 10000
+ EXPECT_FALSE(TimeUtil::FromString("10000-01-01T00:00:00Z", &t));
+ // Valid inputs still work
+ EXPECT_TRUE(TimeUtil::FromString("0001-01-01T00:00:00Z", &t));
+ EXPECT_TRUE(TimeUtil::FromString("9999-12-31T23:59:59Z", &t));
+}
+
} // namespace
} // namespace util
} // namespace protobuf