Skip to content

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

@meng-xu-cs

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions