Summary
parse_rfc3339_weak is documented as UTC-only, but when fractional seconds are present it accepts any trailing 6-byte suffix that starts with + and silently ignores it as if it were +00:00.
That means visibly non-UTC or even malformed inputs such as +01:00, +99:99, or +ab:cd are accepted and parsed as the same instant as +00:00.
Reproduction
use humantime::parse_rfc3339_weak;
use std::time::UNIX_EPOCH;
fn main() {
// control case: the non-fractional path rejects non-UTC offsets
assert!(parse_rfc3339_weak("2018-02-14T00:28:07+01:00").is_err());
for s in [
"2018-02-14T00:28:07.1+00:00",
"2018-02-14T00:28:07.1+01:00",
"2018-02-14T00:28:07.1+99:99",
"2018-02-14T00:28:07.1+ab:cd",
] {
let ts = parse_rfc3339_weak(s).unwrap();
println!(
"{} -> {:?}",
s,
ts.duration_since(UNIX_EPOCH).unwrap()
);
}
}
Expected behavior
Because parse_rfc3339_weak says localized timestamps are unsupported and only UTC is supported, it should only accept:
- no timezone suffix
Z
+00:00
Inputs such as these should return an error:
2018-02-14T00:28:07.1+01:00
2018-02-14T00:28:07.1+99:99
2018-02-14T00:28:07.1+ab:cd
Actual behavior
All of the inputs above are accepted on the fractional-seconds path.
In particular, 2018-02-14T00:28:07.1+01:00 is parsed as the same instant as 2018-02-14T00:28:07.1+00:00, instead of being rejected.
Why this happens
In src/date.rs, the fractional-seconds loop treats + as a generic terminator and only checks that it appears 6 bytes from the end:
} else if b[idx] == b'+' {
// start of "+00:00", which must be at the end
if idx == b.len() - 6 {
break;
}
return Err(Error::InvalidDigit);
}
So the code never verifies that the suffix is actually +00:00.
The non-fractional branch already does the stricter check:
} else if b.len() != 19 && (b.len() > 25 || (b[19] != b'Z' && (&b[19..] != b"+00:00"))) {
return Err(Error::InvalidFormat);
}
Impact
This is silent timestamp corruption in a public parser intended for human input:
- inputs that visibly encode a non-UTC offset are accepted,
- the suffix is ignored,
- the returned
SystemTime is interpreted as UTC with no offset adjustment.
For example, if offsets were supported, 2018-02-14T00:28:07.1+01:00 would correspond to 2018-02-13T23:28:07.1Z; instead it is currently parsed as 2018-02-14T00:28:07.1Z.
Suggested fix
Validate the exact suffix in the fractional-seconds branch instead of only checking the position of +. For example:
} else if b[idx] == b'+' {
if &b[idx..] == b"+00:00" {
break;
}
return Err(Error::InvalidFormat);
}
It would also be good to add regression tests that call parse_rfc3339_weak (not parse_rfc3339) for the invalid cases, for example:
assert!(parse_rfc3339_weak("1970-01-01 00:00:00.0000123+02:00").is_err());
assert!(parse_rfc3339_weak("1970-01-01 00:00:00.0000123+99:99").is_err());
assert!(parse_rfc3339_weak("1970-01-01 00:00:00.0000123+ab:cd").is_err());
Optional follow-up: the same code path also appears to accept a bare . with no fractional digits before Z / +00:00 / +..., which may be worth rejecting in the same branch.
Summary
parse_rfc3339_weakis documented as UTC-only, but when fractional seconds are present it accepts any trailing 6-byte suffix that starts with+and silently ignores it as if it were+00:00.That means visibly non-UTC or even malformed inputs such as
+01:00,+99:99, or+ab:cdare accepted and parsed as the same instant as+00:00.Reproduction
Expected behavior
Because
parse_rfc3339_weaksays localized timestamps are unsupported and only UTC is supported, it should only accept:Z+00:00Inputs such as these should return an error:
2018-02-14T00:28:07.1+01:002018-02-14T00:28:07.1+99:992018-02-14T00:28:07.1+ab:cdActual behavior
All of the inputs above are accepted on the fractional-seconds path.
In particular,
2018-02-14T00:28:07.1+01:00is parsed as the same instant as2018-02-14T00:28:07.1+00:00, instead of being rejected.Why this happens
In
src/date.rs, the fractional-seconds loop treats+as a generic terminator and only checks that it appears 6 bytes from the end:So the code never verifies that the suffix is actually
+00:00.The non-fractional branch already does the stricter check:
Impact
This is silent timestamp corruption in a public parser intended for human input:
SystemTimeis interpreted as UTC with no offset adjustment.For example, if offsets were supported,
2018-02-14T00:28:07.1+01:00would correspond to2018-02-13T23:28:07.1Z; instead it is currently parsed as2018-02-14T00:28:07.1Z.Suggested fix
Validate the exact suffix in the fractional-seconds branch instead of only checking the position of
+. For example:It would also be good to add regression tests that call
parse_rfc3339_weak(notparse_rfc3339) for the invalid cases, for example:Optional follow-up: the same code path also appears to accept a bare
.with no fractional digits beforeZ/+00:00/+..., which may be worth rejecting in the same branch.