Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Do not accept pattern_type from user input in push rules #15088

Merged
merged 12 commits into from
Feb 28, 2023
1 change: 1 addition & 0 deletions changelog.d/15088.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a long-standing bug where Synapse handled an unspecced field on push rules.
9 changes: 3 additions & 6 deletions rust/benches/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ fn bench_match_exact(b: &mut Bencher) {
let condition = Condition::Known(synapse::push::KnownCondition::EventMatch(
EventMatchCondition {
key: "room_id".into(),
pattern: Some("!room:server".into()),
pattern_type: None,
pattern: "!room:server".into(),
},
));

Expand Down Expand Up @@ -109,8 +108,7 @@ fn bench_match_word(b: &mut Bencher) {
let condition = Condition::Known(synapse::push::KnownCondition::EventMatch(
EventMatchCondition {
key: "content.body".into(),
pattern: Some("test".into()),
pattern_type: None,
pattern: "test".into(),
},
));

Expand Down Expand Up @@ -158,8 +156,7 @@ fn bench_match_word_miss(b: &mut Bencher) {
let condition = Condition::Known(synapse::push::KnownCondition::EventMatch(
EventMatchCondition {
key: "content.body".into(),
pattern: Some("foobar".into()),
pattern_type: None,
pattern: "foobar".into(),
},
));

Expand Down
135 changes: 48 additions & 87 deletions rust/src/push/base_rules.rs

Large diffs are not rendered by default.

155 changes: 71 additions & 84 deletions rust/src/push/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};

use crate::push::JsonValue;
use crate::push::{EventMatchPatternType, JsonValue};
use anyhow::{Context, Error};
use lazy_static::lazy_static;
use log::warn;
Expand All @@ -23,8 +24,8 @@ use regex::Regex;

use super::{
utils::{get_glob_matcher, get_localpart_from_id, GlobMatchType},
Action, Condition, EventMatchCondition, ExactEventMatchCondition, FilteredPushRules,
KnownCondition, RelatedEventMatchCondition, SimpleJsonValue,
Action, Condition, ExactEventMatchCondition, FilteredPushRules, KnownCondition,
SimpleJsonValue,
};

lazy_static! {
Expand Down Expand Up @@ -256,14 +257,58 @@ impl PushRuleEvaluator {
};

let result = match known_condition {
KnownCondition::EventMatch(event_match) => {
self.match_event_match(event_match, user_id)?
KnownCondition::EventMatch(event_match) => self.match_event_match(
&self.flattened_keys,
clokep marked this conversation as resolved.
Show resolved Hide resolved
&event_match.key,
&event_match.pattern,
)?,
KnownCondition::EventMatchType(event_match) => {
// The `pattern_type` can either be "user_id" or "user_localpart",
// either way if we don't have a `user_id` then the condition can't
// match.
let user_id = if let Some(user_id) = user_id {
user_id
} else {
return Ok(false);
};

let pattern = match &*event_match.pattern_type {
EventMatchPatternType::UserId => user_id,
EventMatchPatternType::UserLocalpart => get_localpart_from_id(user_id)?,
};

self.match_event_match(&self.flattened_keys, &event_match.key, pattern)?
}
KnownCondition::ExactEventMatch(exact_event_match) => {
self.match_exact_event_match(exact_event_match)?
}
KnownCondition::RelatedEventMatch(event_match) => {
self.match_related_event_match(event_match, user_id)?
KnownCondition::RelatedEventMatch(event_match) => self.match_related_event_match(
&event_match.rel_type.clone(),
event_match.include_fallbacks,
event_match.key.clone(),
event_match.pattern.clone(),
)?,
KnownCondition::RelatedEventMatchType(event_match) => {
// The `pattern_type` can either be "user_id" or "user_localpart",
// either way if we don't have a `user_id` then the condition can't
// match.
let user_id = if let Some(user_id) = user_id {
user_id
} else {
return Ok(false);
};

let pattern = match &*event_match.pattern_type {
EventMatchPatternType::UserId => user_id,
EventMatchPatternType::UserLocalpart => get_localpart_from_id(user_id)?,
};

self.match_related_event_match(
&event_match.rel_type.clone(),
event_match.include_fallbacks,
Some(event_match.key.clone()),
Some(Cow::Borrowed(pattern)),
)?
}
KnownCondition::ExactEventPropertyContains(exact_event_match) => {
self.match_exact_event_property_contains(exact_event_match)?
Expand Down Expand Up @@ -325,32 +370,12 @@ impl PushRuleEvaluator {
/// Evaluates a `event_match` condition.
fn match_event_match(
&self,
event_match: &EventMatchCondition,
user_id: Option<&str>,
flattened_event: &BTreeMap<String, JsonValue>,
key: &str,
pattern: &str,
clokep marked this conversation as resolved.
Show resolved Hide resolved
) -> Result<bool, Error> {
let pattern = if let Some(pattern) = &event_match.pattern {
pattern
} else if let Some(pattern_type) = &event_match.pattern_type {
// The `pattern_type` can either be "user_id" or "user_localpart",
// either way if we don't have a `user_id` then the condition can't
// match.
let user_id = if let Some(user_id) = user_id {
user_id
} else {
return Ok(false);
};

match &**pattern_type {
"user_id" => user_id,
"user_localpart" => get_localpart_from_id(user_id)?,
_ => return Ok(false),
}
} else {
return Ok(false);
};

let haystack = if let Some(JsonValue::Value(SimpleJsonValue::Str(haystack))) =
self.flattened_keys.get(&*event_match.key)
flattened_event.get(key)
{
haystack
} else {
Expand All @@ -359,7 +384,7 @@ impl PushRuleEvaluator {

// For the content.body we match against "words", but for everything
// else we match against the entire value.
let match_type = if event_match.key == "content.body" {
let match_type = if key == "content.body" {
GlobMatchType::Word
} else {
GlobMatchType::Whole
Expand Down Expand Up @@ -395,75 +420,37 @@ impl PushRuleEvaluator {
/// Evaluates a `related_event_match` condition. (MSC3664)
fn match_related_event_match(
&self,
event_match: &RelatedEventMatchCondition,
user_id: Option<&str>,
rel_type: &str,
include_fallbacks: Option<bool>,
key: Option<Cow<str>>,
pattern: Option<Cow<str>>,
) -> Result<bool, Error> {
// First check if related event matching is enabled...
if !self.related_event_match_enabled {
return Ok(false);
}

// get the related event, fail if there is none.
let event = if let Some(event) = self.related_events_flattened.get(&*event_match.rel_type) {
let event = if let Some(event) = self.related_events_flattened.get(rel_type) {
event
} else {
return Ok(false);
};

// If we are not matching fallbacks, don't match if our special key indicating this is a
// fallback relation is not present.
if !event_match.include_fallbacks.unwrap_or(false)
&& event.contains_key("im.vector.is_falling_back")
{
if !include_fallbacks.unwrap_or(false) && event.contains_key("im.vector.is_falling_back") {
return Ok(false);
}

// if we have no key, accept the event as matching, if it existed without matching any
// fields.
let key = if let Some(key) = &event_match.key {
key
} else {
return Ok(true);
};

let pattern = if let Some(pattern) = &event_match.pattern {
pattern
} else if let Some(pattern_type) = &event_match.pattern_type {
// The `pattern_type` can either be "user_id" or "user_localpart",
// either way if we don't have a `user_id` then the condition can't
// match.
let user_id = if let Some(user_id) = user_id {
user_id
} else {
return Ok(false);
};

match &**pattern_type {
"user_id" => user_id,
"user_localpart" => get_localpart_from_id(user_id)?,
_ => return Ok(false),
}
} else {
return Ok(false);
};

let haystack =
if let Some(JsonValue::Value(SimpleJsonValue::Str(haystack))) = event.get(&**key) {
haystack
} else {
return Ok(false);
};

// For the content.body we match against "words", but for everything
// else we match against the entire value.
let match_type = if key == "content.body" {
GlobMatchType::Word
} else {
GlobMatchType::Whole
};

let mut compiled_pattern = get_glob_matcher(pattern, match_type)?;
compiled_pattern.is_match(haystack)
match (key, pattern) {
// if we have no key, accept the event as matching.
(None, _) => Ok(true),
// There was a key, so we *must* have a pattern to go with it.
(Some(_), None) => Ok(false),
// If there is a key & pattern, check if they're in the flattened event (given by rel_type).
(Some(key), Some(pattern)) => self.match_event_match(event, &key, &pattern),
}
}

/// Evaluates a `exact_event_property_contains` condition. (MSC3758)
Expand Down
44 changes: 36 additions & 8 deletions rust/src/push/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,16 @@ pub enum Condition {
#[serde(tag = "kind")]
pub enum KnownCondition {
EventMatch(EventMatchCondition),
// Identical to event_match but gives predefined patterns. Cannot be added by users.
#[serde(skip_deserializing, rename = "event_match")]
EventMatchType(EventMatchTypeCondition),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a test that this serializes correctly, for paranoia.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I added deserialization ones too.

#[serde(rename = "com.beeper.msc3758.exact_event_match")]
ExactEventMatch(ExactEventMatchCondition),
#[serde(rename = "im.nheko.msc3664.related_event_match")]
RelatedEventMatch(RelatedEventMatchCondition),
// Identical to related_event_match but gives predefined patterns. Cannot be added by users.
#[serde(skip_deserializing, rename = "im.nheko.msc3664.related_event_match")]
RelatedEventMatchType(RelatedEventMatchTypeCondition),
#[serde(rename = "org.matrix.msc3966.exact_event_property_contains")]
ExactEventPropertyContains(ExactEventMatchCondition),
#[serde(rename = "org.matrix.msc3952.is_user_mention")]
Expand Down Expand Up @@ -362,14 +368,27 @@ impl<'source> FromPyObject<'source> for Condition {
}
}

/// The body of a [`Condition::EventMatch`]
/// The body of a [`Condition::EventMatch`] with a pattern.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct EventMatchCondition {
pub key: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern_type: Option<Cow<'static, str>>,
pub pattern: Cow<'static, str>,
}

#[derive(Serialize, Debug, Clone)]
#[serde(rename_all = "snake_case")]
pub enum EventMatchPatternType {
UserId,
UserLocalpart,
}

/// The body of a [`Condition::EventMatch`] that uses user_id or user_localpart as a pattern.
#[derive(Serialize, Debug, Clone)]
pub struct EventMatchTypeCondition {
pub key: Cow<'static, str>,
// During serialization, the pattern_type property gets replaced with a
// pattern property of the correct value in synapse.push.clientformat.format_push_rules_for_user.
pub pattern_type: Cow<'static, EventMatchPatternType>,
}

/// The body of a [`Condition::ExactEventMatch`]
Expand All @@ -386,8 +405,18 @@ pub struct RelatedEventMatchCondition {
pub key: Option<Cow<'static, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<Cow<'static, str>>,
pub rel_type: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern_type: Option<Cow<'static, str>>,
pub include_fallbacks: Option<bool>,
}

/// The body of a [`Condition::RelatedEventMatch`] that uses user_id or user_localpart as a pattern.
#[derive(Serialize, Debug, Clone)]
pub struct RelatedEventMatchTypeCondition {
// This is only used if pattern_type exists (and thus key must exist), so is
// a bit simpler than RelatedEventMatchCondition.
pub key: Cow<'static, str>,
pub pattern_type: Cow<'static, EventMatchPatternType>,
pub rel_type: Cow<'static, str>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_fallbacks: Option<bool>,
Expand Down Expand Up @@ -571,8 +600,7 @@ impl FilteredPushRules {
fn test_serialize_condition() {
let condition = Condition::Known(KnownCondition::EventMatch(EventMatchCondition {
key: "content.body".into(),
pattern: Some("coffee".into()),
pattern_type: None,
pattern: "coffee".into(),
}));

let json = serde_json::to_string(&condition).unwrap();
Expand Down
1 change: 1 addition & 0 deletions synapse/push/clientformat.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def format_push_rules_for_user(
rulearray.append(template_rule)

pattern_type = template_rule.pop("pattern_type", None)
print(pattern_type)
clokep marked this conversation as resolved.
Show resolved Hide resolved
if pattern_type == "user_id":
template_rule["pattern"] = user.to_string()
elif pattern_type == "user_localpart":
Expand Down
27 changes: 27 additions & 0 deletions tests/push/test_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,33 @@ def test_event_match_non_body(self) -> None:
"pattern should not match before a newline",
)

def test_event_match_pattern(self) -> None:
"""Check that event_match conditions do not use a "pattern_type" from user data."""

# The pattern_type should not be deserialized into anything valid.
condition = {
"kind": "event_match",
"key": "content.value",
"pattern_type": "user_id",
}
self._assert_not_matches(
condition,
{"value": "@user:test"},
"should not be possible to pass a pattern_type in",
)

# This is an internal-only condition which shouldn't get deserialized.
condition = {
"kind": "event_match_type",
"key": "content.value",
"pattern_type": "user_id",
}
self._assert_not_matches(
condition,
{"value": "@user:test"},
"should not be possible to pass a pattern_type in",
)

def test_exact_event_match_string(self) -> None:
"""Check that exact_event_match conditions work as expected for strings."""

Expand Down