Skip to content

parse_rfc3339_weak accepts any 6-byte +... suffix after fractional seconds and treats it as UTC #67

Description

@meng-xu-cs

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions