Logins: Validate and fixup hostname and formSubmitURL. (#2380)
This commit is contained in:
Родитель
094fd42d02
Коммит
0478aadc00
|
@ -147,12 +147,11 @@ class DatabaseLoginsStorageTest : LoginsStorageTest() {
|
|||
assert(!LoginsStoreMetrics.writeQueryErrorCount["invalid_record"].testHasValue())
|
||||
|
||||
try {
|
||||
// N.B. this is invalid due to both `httpRealm` and `formSubmitURL`
|
||||
// N.B. this is invalid due to `formSubmitURL` being an invalid url.
|
||||
store.add(ServerPassword(
|
||||
id = "bbbbbbbbbbbb",
|
||||
hostname = "https://test.example.com",
|
||||
httpRealm = "Something",
|
||||
formSubmitURL = "https://www.example.com",
|
||||
formSubmitURL = "not a url",
|
||||
username = "Foobar2000",
|
||||
password = "hunter2",
|
||||
usernameField = "users_name",
|
||||
|
|
|
@ -365,12 +365,11 @@ abstract class LoginsStorageTest {
|
|||
|
||||
companion object {
|
||||
val INVALID_RECORDS: List<ServerPassword> = listOf(
|
||||
// Both formSubmitURL and httpRealm
|
||||
// Invalid formSubmitURL
|
||||
ServerPassword(
|
||||
id = "",
|
||||
hostname = "https://www.foo.org",
|
||||
httpRealm = "Test Realm",
|
||||
formSubmitURL = "https://www.foo.org/login",
|
||||
formSubmitURL = "invalid\u0000value",
|
||||
password = "MyPassword",
|
||||
username = "MyUsername",
|
||||
usernameField = "users_name",
|
||||
|
|
|
@ -1194,7 +1194,7 @@ mod tests {
|
|||
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
|
||||
db.add(Login {
|
||||
guid: "dummy_000001".into(),
|
||||
form_submit_url: Some("https://www.example.com/submit".into()),
|
||||
form_submit_url: Some("https://www.example.com".into()),
|
||||
hostname: "https://www.example.com".into(),
|
||||
http_realm: None,
|
||||
username: "test".into(),
|
||||
|
@ -1205,7 +1205,7 @@ mod tests {
|
|||
|
||||
let duplicate_login_check = db.check_valid_with_no_dupes(&Login {
|
||||
guid: Guid::empty(),
|
||||
form_submit_url: Some("https://www.example.com/submit".into()),
|
||||
form_submit_url: Some("https://www.example.com".into()),
|
||||
hostname: "https://www.example.com".into(),
|
||||
http_realm: None,
|
||||
username: "test".into(),
|
||||
|
@ -1220,12 +1220,60 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_submit() {
|
||||
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
|
||||
db.add(Login {
|
||||
guid: "dummy_000001".into(),
|
||||
form_submit_url: Some("http://😍.com".into()),
|
||||
hostname: "http://😍.com".into(),
|
||||
http_realm: None,
|
||||
username: "😍".into(),
|
||||
username_field: "😍".into(),
|
||||
password: "😍".into(),
|
||||
password_field: "😍".into(),
|
||||
..Login::default()
|
||||
})
|
||||
.unwrap();
|
||||
let fetched = db
|
||||
.get_by_id("dummy_000001")
|
||||
.expect("should work")
|
||||
.expect("should get a record");
|
||||
assert_eq!(fetched.hostname, "http://xn--r28h.com");
|
||||
assert_eq!(fetched.form_submit_url.unwrap(), "http://xn--r28h.com");
|
||||
assert_eq!(fetched.username, "😍");
|
||||
assert_eq!(fetched.username_field, "😍");
|
||||
assert_eq!(fetched.password, "😍");
|
||||
assert_eq!(fetched.password_field, "😍");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_realm() {
|
||||
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
|
||||
db.add(Login {
|
||||
guid: "dummy_000001".into(),
|
||||
form_submit_url: None,
|
||||
hostname: "http://😍.com".into(),
|
||||
http_realm: Some("😍😍".into()),
|
||||
username: "😍".into(),
|
||||
password: "😍".into(),
|
||||
..Login::default()
|
||||
})
|
||||
.unwrap();
|
||||
let fetched = db
|
||||
.get_by_id("dummy_000001")
|
||||
.expect("should work")
|
||||
.expect("should get a record");
|
||||
assert_eq!(fetched.hostname, "http://xn--r28h.com");
|
||||
assert_eq!(fetched.http_realm.unwrap(), "😍😍");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_valid_with_no_dupes_with_unique_login() {
|
||||
let db = LoginDb::open_in_memory(Some("testing")).unwrap();
|
||||
db.add(Login {
|
||||
guid: "dummy_000001".into(),
|
||||
form_submit_url: Some("https://www.example.com/submit".into()),
|
||||
form_submit_url: Some("https://www.example.com".into()),
|
||||
hostname: "https://www.example.com".into(),
|
||||
http_realm: None,
|
||||
username: "test".into(),
|
||||
|
@ -1306,10 +1354,6 @@ mod tests {
|
|||
"https://sub.example.com:8080",
|
||||
"https://sub.sub.example.com",
|
||||
"ftp://sub.example.com",
|
||||
// Handling file:// is questionable - it would also be fine to
|
||||
// not handle it! It's left here more to document the fact that
|
||||
// we do!
|
||||
"file://example.com",
|
||||
],
|
||||
vec![
|
||||
"https://badexample.com",
|
||||
|
@ -1323,8 +1367,7 @@ mod tests {
|
|||
// on insert.
|
||||
check_good_bad(
|
||||
vec![
|
||||
"http://😍.com",
|
||||
"http://xn--r28h.com", // punycoded version of the above.
|
||||
"http://xn--r28h.com", // punycoded version of "http://😍.com"
|
||||
],
|
||||
vec!["http://💖.com"],
|
||||
vec!["😍.com", "xn--r28h.com"],
|
||||
|
@ -1345,11 +1388,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_get_by_base_domain_ipv6() {
|
||||
check_good_bad(
|
||||
vec![
|
||||
"http://[::1]",
|
||||
"https://[::1]:8000",
|
||||
"https://[0:0:0:0:0:0:0:1]", // this is [::1]
|
||||
],
|
||||
vec!["http://[::1]", "https://[::1]:8000"],
|
||||
vec!["https://[0:0:0:0:0:0:1:1]", "https://example.com"],
|
||||
vec!["[::1]", "[0:0:0:0:0:0:0:1]"],
|
||||
vec!["[0:0:0:0:0:0:1:2]"],
|
||||
|
|
|
@ -172,7 +172,7 @@ mod test {
|
|||
let a = Login {
|
||||
guid: "aaaaaaaaaaaa".into(),
|
||||
hostname: "https://www.example.com".into(),
|
||||
form_submit_url: Some("https://www.example.com/login".into()),
|
||||
form_submit_url: Some("https://www.example.com".into()),
|
||||
username: "coolperson21".into(),
|
||||
password: "p4ssw0rd".into(),
|
||||
username_field: "user_input".into(),
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
//! - "ftp://ftp.site.com"
|
||||
//! - "moz-proxy://127.0.0.1:8888"
|
||||
//! - "chrome://MyLegacyExtension"
|
||||
//! - "file:///"
|
||||
//! - "file://"
|
||||
//! - "https://[::1]"
|
||||
//!
|
||||
//! If invalid data is received in this field (either from the application, or via sync)
|
||||
//! then the logins store will attempt to coerce it into valid data by:
|
||||
|
@ -47,33 +48,21 @@
|
|||
//! - converting values with non-ascii characters into punycode
|
||||
//!
|
||||
//! **XXX TODO:**
|
||||
//! - test that we validate as an origin.
|
||||
//! - test that we fixup full URLs by turning them into origins?
|
||||
//! - the thing where we normalize to punycode, including tests
|
||||
//! - how to provide the non-punycode version to the app easily?
|
||||
//! - the great renaming (maybe we can do the punycode thing at the same time?)
|
||||
//! - return a "display" field (exact name TBD) in the serialized
|
||||
//! version, which will be the unicode version of punycode urls.
|
||||
//! - the great renaming
|
||||
//!
|
||||
//! - `password`: The saved password, as a string.
|
||||
//!
|
||||
//! This field is required, and must not be set to the empty string. It must not contain
|
||||
//! null bytes, but can otherwise be an arbitrary unicode string.
|
||||
//!
|
||||
//! **XXX TODO:**
|
||||
//! - test that it cannot by null or the empty string
|
||||
//! - test that it cannot contain null bytes (do we want to fixup by just removing them?)
|
||||
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
|
||||
//!
|
||||
//! - `username`: The username associated with this login, if any, as a string.
|
||||
//!
|
||||
//! This field is required, but may be set to the empty string if no username is associated
|
||||
//! with the login. It must not contain null bytes, but can otherwise be an arbitrary unicode
|
||||
//! string.
|
||||
//!
|
||||
//! **XXX TODO:**:
|
||||
//! - test that it cannot by null
|
||||
//! - test that it cannot contain null bytes (do we want to fixup by just removing them?)
|
||||
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
|
||||
//!
|
||||
//! - `httpRealm`: The challenge string for HTTP Basic authentication, if any.
|
||||
//!
|
||||
//! If present, the login should only be used in response to a HTTP Basic Auth
|
||||
|
@ -86,11 +75,6 @@
|
|||
//! of login (HTTP-Auth based versus form-based). Exactly one of `httpRealm` and `formSubmitURL`
|
||||
//! must be present.
|
||||
//!
|
||||
//! **XXX TODO**:
|
||||
//! - test that it cannot contain null bytes, carriage returns or newlines (do we want to fixup by just removing them?)
|
||||
//! - test that it cannot be present with formSubmitURL
|
||||
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
|
||||
//!
|
||||
//! - `formSubmitURL`: The target origin of forms in which this login can be used, if any, as a string.
|
||||
//!
|
||||
//! If present, the login should only be used in forms whose target submission URL matches this origin.
|
||||
|
@ -112,15 +96,8 @@
|
|||
//! - replacing invalid values with null if a valid 'httpRealm' field is present
|
||||
//!
|
||||
//! **XXX TODO**:
|
||||
//! - test that we validate as an origin.
|
||||
//! - test that we fixup full URLs by turning them into origins?
|
||||
//! - test that we fixup "." to the empty string
|
||||
//! - test that we allow the special "javascript:" value
|
||||
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
|
||||
//! - test that it cannot be present with 'httpRealm'
|
||||
//! - test that we set invalid values to null when 'httpRealm' is present
|
||||
//! - the thing where we normalize to punycode, including tests
|
||||
//! - how to provide the non-punycode version to the app easily?
|
||||
//! - return a "display" field (exact name TBD) in the serialized
|
||||
//! version, which will be the unicode version of punycode urls.
|
||||
//! - the great renaming (maybe we can do the punycode thing at the same time?)
|
||||
//!
|
||||
//! - `usernameField`: The name of the form field into which the 'username' should be filled, if any.
|
||||
|
@ -133,10 +110,6 @@
|
|||
//! then the logins store will attempt to coerce it into valid data by:
|
||||
//! - setting to the empty string if 'formSubmitURL' is not present
|
||||
//!
|
||||
//! **XXX TODO:**
|
||||
//! - test that we reject invalid characters; should we fix them up at all?
|
||||
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
|
||||
//!
|
||||
//! - `passwordField`: The name of the form field into which the 'password' should be filled, if any.
|
||||
//!
|
||||
//! This value is stored if provided by the application, but does not imply any restrictions on
|
||||
|
@ -147,10 +120,6 @@
|
|||
//! then the logins store will attempt to coerce it into valid data by:
|
||||
//! - setting to the empty string if 'formSubmitURL' is not present
|
||||
//!
|
||||
//! **XXX TODO:**
|
||||
//! - test that we reject invalid characters; should we fix them up at all?
|
||||
//! - test that it *can* contain non-ascii characters (esp. when round-tripped through the db)
|
||||
//!
|
||||
//! - `timesUsed`: A lower bound on the number of times the password from this record has been used, as an integer.
|
||||
//!
|
||||
//! Applications should use the `touch()` method of the logins store to indicate when a password
|
||||
|
@ -173,7 +142,7 @@
|
|||
//! - replacing missing or negative values with 0
|
||||
//!
|
||||
//! **XXX TODO:**
|
||||
//! - test that we prevent this timestamp from moving backwards.
|
||||
//! - test that we prevent this counter from moving backwards.
|
||||
//! - test fixups of missing or negative values
|
||||
//! - test that we correctly merge dupes
|
||||
//!
|
||||
|
@ -249,13 +218,13 @@
|
|||
//!
|
||||
//! In order to deal with data from legacy clients in a robust way, it is necessary to be able to build
|
||||
//! and manipulate `Login` structs that contain invalid data. The following methods can be used by callers
|
||||
//! to ensure that they're only work with valid records:
|
||||
//! to ensure that they're only working with valid records:
|
||||
//!
|
||||
//! - `Login::check_valid()`: Checks valdity of a login record, returning `()` if it is valid
|
||||
//! or an error if it is not.
|
||||
//!
|
||||
//! - `Login::fixup()`: Returns either the existing login if it is valid, a clone with invalid fields
|
||||
//! fixed up if it was safe to do so, or an error if the login is irreperably invalid.
|
||||
//! fixed up if it was safe to do so, or an error if the login is irreparably invalid.
|
||||
|
||||
use crate::error::*;
|
||||
use crate::util;
|
||||
|
@ -264,6 +233,7 @@ use serde_derive::*;
|
|||
use std::time::{self, SystemTime};
|
||||
use sync15::ServerTimestamp;
|
||||
use sync_guid::Guid;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -361,6 +331,54 @@ impl Login {
|
|||
self.validate_and_fixup(true)
|
||||
}
|
||||
|
||||
/// Internal helper for validation and fixups of an "origin" stored as
|
||||
/// a string.
|
||||
fn validate_and_fixup_origin(origin: &str) -> Result<Option<String>> {
|
||||
// Check we can parse the origin, then use the normalized version of it.
|
||||
match Url::parse(&origin) {
|
||||
Ok(mut u) => {
|
||||
// Presumably this is a faster path than always setting?
|
||||
if u.path() != "/"
|
||||
|| u.fragment().is_some()
|
||||
|| u.query().is_some()
|
||||
|| u.username() != "/"
|
||||
|| u.password().is_some()
|
||||
{
|
||||
// Not identical - we only want the origin part, so kill
|
||||
// any other parts which may exist.
|
||||
// But first special case `file://` URLs which always
|
||||
// resolve to `file://`
|
||||
if u.scheme() == "file" {
|
||||
return Ok(if origin == "file://" {
|
||||
None
|
||||
} else {
|
||||
Some("file://".into())
|
||||
});
|
||||
}
|
||||
u.set_path("");
|
||||
u.set_fragment(None);
|
||||
u.set_query(None);
|
||||
let _ = u.set_username("");
|
||||
let _ = u.set_password(None);
|
||||
// We always store without the trailing "/" which Urls have.
|
||||
let mut href = u.into_string();
|
||||
href.pop().expect("url must have a length");
|
||||
if origin != href {
|
||||
// Needs to be fixed up.
|
||||
return Ok(Some(href));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
Err(_) => {
|
||||
// We can't fixup completely invalid records, so always throw.
|
||||
throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "Origin is Malformed".into()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal helper for doing validation and fixups.
|
||||
fn validate_and_fixup(&self, fixup: bool) -> Result<Option<Self>> {
|
||||
// XXX TODO: we've definitely got more validation and fixups to add here!
|
||||
|
@ -393,7 +411,7 @@ impl Login {
|
|||
}
|
||||
|
||||
if self.form_submit_url.is_some() && self.http_realm.is_some() {
|
||||
throw!(InvalidLogin::BothTargets);
|
||||
get_fixed_or_throw!(InvalidLogin::BothTargets)?.http_realm = None;
|
||||
}
|
||||
|
||||
if self.form_submit_url.is_none() && self.http_realm.is_none() {
|
||||
|
@ -401,7 +419,12 @@ impl Login {
|
|||
}
|
||||
|
||||
let form_submit_url = self.form_submit_url.clone().unwrap_or_default();
|
||||
let http_realm = self.http_realm.clone().unwrap_or_default();
|
||||
let http_realm = maybe_fixed
|
||||
.as_ref()
|
||||
.unwrap_or(self)
|
||||
.http_realm
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
|
||||
let field_data = [
|
||||
("formSubmitUrl", &form_submit_url),
|
||||
|
@ -440,32 +463,49 @@ impl Login {
|
|||
});
|
||||
}
|
||||
|
||||
if form_submit_url == "." {
|
||||
throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "`formSubmitUrl` is a period".into()
|
||||
});
|
||||
// Check we can parse the origin, then use the normalized version of it.
|
||||
if let Some(fixed) = Login::validate_and_fixup_origin(&self.hostname)? {
|
||||
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "Origin is not normalized".into()
|
||||
})?
|
||||
.hostname = fixed;
|
||||
}
|
||||
|
||||
if self.hostname.contains(" (") {
|
||||
throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "Origin is Malformed".into()
|
||||
});
|
||||
}
|
||||
|
||||
if self.form_submit_url.is_none() {
|
||||
if !self.username_field.is_empty() {
|
||||
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "usernameField must be empty when formSubmitURL is null".into()
|
||||
})?
|
||||
.username_field
|
||||
.clear();
|
||||
match &maybe_fixed.as_ref().unwrap_or(self).form_submit_url {
|
||||
None => {
|
||||
if !self.username_field.is_empty() {
|
||||
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "usernameField must be empty when formSubmitURL is null".into()
|
||||
})?
|
||||
.username_field
|
||||
.clear();
|
||||
}
|
||||
if !self.password_field.is_empty() {
|
||||
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "passwordField must be empty when formSubmitURL is null".into()
|
||||
})?
|
||||
.password_field
|
||||
.clear();
|
||||
}
|
||||
}
|
||||
if !self.password_field.is_empty() {
|
||||
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "passwordField must be empty when formSubmitURL is null".into()
|
||||
})?
|
||||
.password_field
|
||||
.clear();
|
||||
Some(href) => {
|
||||
// "." and "javascript:" are special cases documented at the top of this file.
|
||||
if href == "." {
|
||||
// A bit of a special case - if we are being asked to fixup, we replace
|
||||
// "." with an empty string - but if not fixing up we don't complain.
|
||||
if fixup {
|
||||
maybe_fixed
|
||||
.get_or_insert_with(|| self.clone())
|
||||
.form_submit_url = Some("".into());
|
||||
}
|
||||
} else if href != "javascript:" {
|
||||
if let Some(fixed) = Login::validate_and_fixup_origin(&href)? {
|
||||
get_fixed_or_throw!(InvalidLogin::IllegalFieldValue {
|
||||
field_info: "formActionOrigin is not normalized".into()
|
||||
})?
|
||||
.form_submit_url = Some(fixed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -890,6 +930,58 @@ mod tests {
|
|||
assert_eq!(login.time_password_changed, now64 - 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_url_fixups() -> Result<()> {
|
||||
// Start with URLs which are all valid and already normalized.
|
||||
for input in &[
|
||||
// The list of valid hostnames documented at the top of this file.
|
||||
"https://site.com",
|
||||
"http://site.com:1234",
|
||||
"ftp://ftp.site.com",
|
||||
"moz-proxy://127.0.0.1:8888",
|
||||
"chrome://MyLegacyExtension",
|
||||
"file://",
|
||||
"https://[::1]",
|
||||
] {
|
||||
assert_eq!(Login::validate_and_fixup_origin(input)?, None);
|
||||
}
|
||||
|
||||
// And URLs which get normalized.
|
||||
for (input, output) in &[
|
||||
("https://site.com/", "https://site.com"),
|
||||
("http://site.com:1234/", "http://site.com:1234"),
|
||||
("http://example.com/foo?query=wtf#bar", "http://example.com"),
|
||||
("http://example.com/foo#bar", "http://example.com"),
|
||||
(
|
||||
"http://username:password@example.com/",
|
||||
"http://example.com",
|
||||
),
|
||||
("http://😍.com/", "http://xn--r28h.com"),
|
||||
("https://[0:0:0:0:0:0:0:1]", "https://[::1]"),
|
||||
// All `file://` URLs normalize to exactly `file://`. See #2384 for
|
||||
// why we might consider changing that later.
|
||||
("file:///", "file://"),
|
||||
("file://foo/bar", "file://"),
|
||||
("file://foo/bar/", "file://"),
|
||||
("moz-proxy://127.0.0.1:8888/", "moz-proxy://127.0.0.1:8888"),
|
||||
(
|
||||
"moz-proxy://127.0.0.1:8888/foo",
|
||||
"moz-proxy://127.0.0.1:8888",
|
||||
),
|
||||
("chrome://MyLegacyExtension/", "chrome://MyLegacyExtension"),
|
||||
(
|
||||
"chrome://MyLegacyExtension/foo",
|
||||
"chrome://MyLegacyExtension",
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
Login::validate_and_fixup_origin(input)?,
|
||||
Some((*output).into())
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_valid() {
|
||||
struct TestCase {
|
||||
|
@ -936,7 +1028,7 @@ mod tests {
|
|||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_null_html_realm = Login {
|
||||
let login_with_null_http_realm = Login {
|
||||
hostname: "https://www.example.com".into(),
|
||||
http_realm: Some("https://www.example.\0com".into()),
|
||||
username: "test".into(),
|
||||
|
@ -952,6 +1044,14 @@ mod tests {
|
|||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_null_password = Login {
|
||||
hostname: "https://www.example.com".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
username: "username".into(),
|
||||
password: "test\0".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_newline_hostname = Login {
|
||||
hostname: "\rhttps://www.example.com".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
|
@ -969,6 +1069,14 @@ mod tests {
|
|||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_newline_realm = Login {
|
||||
hostname: "https://www.example.com".into(),
|
||||
http_realm: Some("foo\nbar".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_newline_password = Login {
|
||||
hostname: "https://www.example.com".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
|
@ -994,6 +1102,14 @@ mod tests {
|
|||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_javascript_form_submit_url = Login {
|
||||
form_submit_url: Some("javascript:".into()),
|
||||
hostname: "https://www.example.com".into(),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_malformed_origin_parens = Login {
|
||||
hostname: " (".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
|
@ -1002,6 +1118,38 @@ mod tests {
|
|||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_host_unicode = Login {
|
||||
hostname: "http://💖.com".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_hostname_trailing_slash = Login {
|
||||
hostname: "https://www.example.com/".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_hostname_expanded_ipv6 = Login {
|
||||
hostname: "https://[0:0:0:0:0:0:1:1]".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_unknown_protocol = Login {
|
||||
hostname: "moz-proxy://127.0.0.1:8888".into(),
|
||||
http_realm: Some("https://www.example.com".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
login: valid_login,
|
||||
|
@ -1029,7 +1177,7 @@ mod tests {
|
|||
expected_err: "Invalid login: Neither `formSubmitUrl` or `httpRealm` are present",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_null_html_realm,
|
||||
login: login_with_null_http_realm,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: `httpRealm` contains Nul",
|
||||
},
|
||||
|
@ -1038,11 +1186,22 @@ mod tests {
|
|||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: `username` contains Nul",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_null_password,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: `password` contains Nul",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_newline_hostname,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: `hostname` contains newline",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_newline_realm,
|
||||
should_err: true,
|
||||
expected_err:
|
||||
"Invalid login: Login has illegal field: `httpRealm` contains newline",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_newline_username_field,
|
||||
should_err: true,
|
||||
|
@ -1061,14 +1220,39 @@ mod tests {
|
|||
},
|
||||
TestCase {
|
||||
login: login_with_period_form_submit_url,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: `formSubmitUrl` is a period",
|
||||
should_err: false,
|
||||
expected_err: "",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_javascript_form_submit_url,
|
||||
should_err: false,
|
||||
expected_err: "",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_malformed_origin_parens,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: Origin is Malformed",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_host_unicode,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_hostname_trailing_slash,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_hostname_expanded_ipv6,
|
||||
should_err: true,
|
||||
expected_err: "Invalid login: Login has illegal field: Origin is not normalized",
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_unknown_protocol,
|
||||
should_err: false,
|
||||
expected_err: "",
|
||||
},
|
||||
];
|
||||
|
||||
for tc in &test_cases {
|
||||
|
@ -1083,6 +1267,84 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fixup() {
|
||||
#[derive(Default)]
|
||||
struct TestCase {
|
||||
login: Login,
|
||||
fixedup_host: Option<&'static str>,
|
||||
fixedup_form_submit_url: Option<String>,
|
||||
}
|
||||
|
||||
// Note that most URL fixups are tested above, but we have one or 2 here.
|
||||
let login_with_full_url = Login {
|
||||
hostname: "http://example.com/foo?query=wtf#bar".into(),
|
||||
form_submit_url: Some("http://example.com/foo?query=wtf#bar".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_host_unicode = Login {
|
||||
hostname: "http://😍.com".into(),
|
||||
form_submit_url: Some("http://😍.com".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_period_fsu = Login {
|
||||
hostname: "https://example.com".into(),
|
||||
form_submit_url: Some(".".into()),
|
||||
username: "test".into(),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let login_with_form_submit_and_http_realm = Login {
|
||||
hostname: "https://www.example.com".into(),
|
||||
form_submit_url: Some("https://www.example.com".into()),
|
||||
// If both http_realm and form_submit_url are specified, we drop
|
||||
// the former when fixing up. So for this test we must have an
|
||||
// invalid value in http_realm to ensure we don't validate a value
|
||||
// we end up dropping.
|
||||
http_realm: Some("\n".into()),
|
||||
password: "test".into(),
|
||||
..Login::default()
|
||||
};
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
login: login_with_full_url,
|
||||
fixedup_host: "http://example.com".into(),
|
||||
fixedup_form_submit_url: Some("http://example.com".into()),
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_host_unicode,
|
||||
fixedup_host: "http://xn--r28h.com".into(),
|
||||
fixedup_form_submit_url: Some("http://xn--r28h.com".into()),
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_period_fsu,
|
||||
fixedup_form_submit_url: Some("".into()),
|
||||
..TestCase::default()
|
||||
},
|
||||
TestCase {
|
||||
login: login_with_form_submit_and_http_realm,
|
||||
fixedup_form_submit_url: Some("https://www.example.com".into()),
|
||||
..TestCase::default()
|
||||
},
|
||||
];
|
||||
|
||||
for tc in &test_cases {
|
||||
let login = tc.login.clone().fixup().expect("should work");
|
||||
if let Some(expected) = tc.fixedup_host {
|
||||
assert_eq!(login.hostname, expected);
|
||||
}
|
||||
assert_eq!(login.form_submit_url, tc.fixedup_form_submit_url);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_username_field_requires_a_form_target() {
|
||||
let bad_payload: sync15::Payload = serde_json::from_value(serde_json::json!({
|
||||
|
|
|
@ -63,7 +63,8 @@ class LoginsTests: XCTestCase {
|
|||
|
||||
let record0 = try! storage.get(id: id0)!
|
||||
XCTAssertNil(record0.httpRealm)
|
||||
XCTAssertEqual(record0.formSubmitURL, "https://www.example.com/login")
|
||||
// We fixed up the formSubmitURL to just be the origin part of the url.
|
||||
XCTAssertEqual(record0.formSubmitURL, "https://www.example.com")
|
||||
|
||||
let id1 = try! storage.add(login: LoginRecord(
|
||||
id: "",
|
||||
|
@ -95,7 +96,7 @@ class LoginsTests: XCTestCase {
|
|||
password: "hunter5",
|
||||
hostname: "https://www.example5.com",
|
||||
username: "cooluser55",
|
||||
formSubmitURL: "https://www.example5.com/login",
|
||||
formSubmitURL: "https://www.example5.com",
|
||||
httpRealm: nil,
|
||||
timesUsed: nil,
|
||||
timeLastUsed: nil,
|
||||
|
@ -110,7 +111,7 @@ class LoginsTests: XCTestCase {
|
|||
password: "hunter3",
|
||||
hostname: "https://www.example5.com",
|
||||
username: "cooluser55",
|
||||
formSubmitURL: "https://www.example5.com/login",
|
||||
formSubmitURL: "https://www.example5.com",
|
||||
httpRealm: nil,
|
||||
timesUsed: nil,
|
||||
timeLastUsed: nil,
|
||||
|
@ -125,7 +126,7 @@ class LoginsTests: XCTestCase {
|
|||
password: "hunter3",
|
||||
hostname: "https://www.example6.com",
|
||||
username: "\0cooluser56",
|
||||
formSubmitURL: "https://www.example6.com/login",
|
||||
formSubmitURL: "https://www.example6.com",
|
||||
httpRealm: nil,
|
||||
timesUsed: nil,
|
||||
timeLastUsed: nil,
|
||||
|
|
Загрузка…
Ссылка в новой задаче