commit a9e9a397d82c9bf7a7bc8aa1413b925f7450a16a
parent 9824d94a1cc039d1e0ac62301abf0b6fbb148709
Author: Jeremy Lin <jeremy.lin@gmail.com>
Date: Sat, 5 Dec 2020 16:22:20 -0800
Validate cipher updates with revision date
Prevent clients from updating a cipher if the local copy is stale.
Validation is only performed when the client provides its last known
revision date; this date isn't provided when using older clients,
or when the operation doesn't involve updating an existing cipher.
Upstream PR: https://github.com/bitwarden/server/pull/994
Diffstat:
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs
@@ -1,6 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::path::Path;
+use chrono::{NaiveDateTime, Utc};
use rocket::{http::ContentType, request::Form, Data, Route};
use rocket_contrib::json::Json;
use serde_json::Value;
@@ -194,6 +195,14 @@ pub struct CipherData {
#[serde(rename = "Attachments")]
_Attachments: Option<Value>, // Unused, contains map of {id: filename}
Attachments2: Option<HashMap<String, Attachments2Data>>,
+
+ // The revision datetime (in ISO 8601 format) of the client's local copy
+ // of the cipher. This is used to prevent a client from updating a cipher
+ // when it doesn't have the latest version, as that can result in data
+ // loss. It's not an error when no value is provided; this can happen
+ // when using older client versions, or if the operation doesn't involve
+ // updating an existing cipher.
+ LastKnownRevisionDate: Option<String>,
}
#[derive(Deserialize, Debug)]
@@ -238,6 +247,17 @@ pub fn update_cipher_from_data(
nt: &Notify,
ut: UpdateType,
) -> EmptyResult {
+ // Check that the client isn't updating an existing cipher with stale data.
+ if let Some(dt) = data.LastKnownRevisionDate {
+ match NaiveDateTime::parse_from_str(&dt, "%+") { // ISO 8601 format
+ Err(err) =>
+ warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err),
+ Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 =>
+ err!("The client copy of this cipher is out of date. Resync the client and try again."),
+ Ok(_) => (),
+ }
+ }
+
if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.OrganizationId {
err!("Organization mismatch. Please resync the client before updating the cipher")
}
@@ -1030,7 +1050,7 @@ fn _delete_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn, soft_del
}
if soft_delete {
- cipher.deleted_at = Some(chrono::Utc::now().naive_utc());
+ cipher.deleted_at = Some(Utc::now().naive_utc());
cipher.save(&conn)?;
nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn));
} else {