Imported Upstream version 0.33.3.1
Richard van der Hoff
5 years ago
0 | Synapse 0.33.3.1 (2018-09-06) | |
1 | ============================= | |
2 | ||
3 | SECURITY FIXES | |
4 | -------------- | |
5 | ||
6 | - Fix an issue where event signatures were not always correctly validated ([\#3796](https://github.com/matrix-org/synapse/issues/3796)) | |
7 | - Fix an issue where server_acls could be circumvented for incoming events ([\#3796](https://github.com/matrix-org/synapse/issues/3796)) | |
8 | ||
9 | Internal Changes | |
10 | ---------------- | |
11 | ||
12 | - Unignore synctl in .dockerignore to fix docker builds ([\#3802](https://github.com/matrix-org/synapse/issues/3802)) | |
13 | ||
0 | 14 | Synapse 0.33.3 (2018-08-22) |
1 | 15 | =========================== |
2 | 16 |
16 | 16 | """ This is a reference implementation of a Matrix home server. |
17 | 17 | """ |
18 | 18 | |
19 | __version__ = "0.33.3" | |
19 | __version__ = "0.33.3.1" |
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 | import logging |
15 | from collections import namedtuple | |
15 | 16 | |
16 | 17 | import six |
17 | 18 | |
18 | 19 | from twisted.internet import defer |
19 | ||
20 | from synapse.api.constants import MAX_DEPTH | |
20 | from twisted.internet.defer import DeferredList | |
21 | ||
22 | from synapse.api.constants import MAX_DEPTH, EventTypes, Membership | |
21 | 23 | from synapse.api.errors import Codes, SynapseError |
22 | 24 | from synapse.crypto.event_signing import check_event_content_hash |
23 | 25 | from synapse.events import FrozenEvent |
24 | 26 | from synapse.events.utils import prune_event |
25 | 27 | from synapse.http.servlet import assert_params_in_dict |
28 | from synapse.types import get_domain_from_id | |
26 | 29 | from synapse.util import logcontext, unwrapFirstError |
27 | 30 | |
28 | 31 | logger = logging.getLogger(__name__) |
132 | 135 | * throws a SynapseError if the signature check failed. |
133 | 136 | The deferreds run their callbacks in the sentinel logcontext. |
134 | 137 | """ |
135 | ||
136 | redacted_pdus = [ | |
137 | prune_event(pdu) | |
138 | for pdu in pdus | |
139 | ] | |
140 | ||
141 | deferreds = self.keyring.verify_json_objects_for_server([ | |
142 | (p.origin, p.get_pdu_json()) | |
143 | for p in redacted_pdus | |
144 | ]) | |
138 | deferreds = _check_sigs_on_pdus(self.keyring, pdus) | |
145 | 139 | |
146 | 140 | ctx = logcontext.LoggingContext.current_context() |
147 | 141 | |
148 | def callback(_, pdu, redacted): | |
142 | def callback(_, pdu): | |
149 | 143 | with logcontext.PreserveLoggingContext(ctx): |
150 | 144 | if not check_event_content_hash(pdu): |
151 | 145 | logger.warn( |
152 | 146 | "Event content has been tampered, redacting %s: %s", |
153 | 147 | pdu.event_id, pdu.get_pdu_json() |
154 | 148 | ) |
155 | return redacted | |
149 | return prune_event(pdu) | |
156 | 150 | |
157 | 151 | if self.spam_checker.check_event_for_spam(pdu): |
158 | 152 | logger.warn( |
159 | 153 | "Event contains spam, redacting %s: %s", |
160 | 154 | pdu.event_id, pdu.get_pdu_json() |
161 | 155 | ) |
162 | return redacted | |
156 | return prune_event(pdu) | |
163 | 157 | |
164 | 158 | return pdu |
165 | 159 | |
172 | 166 | ) |
173 | 167 | return failure |
174 | 168 | |
175 | for deferred, pdu, redacted in zip(deferreds, pdus, redacted_pdus): | |
169 | for deferred, pdu in zip(deferreds, pdus): | |
176 | 170 | deferred.addCallbacks( |
177 | 171 | callback, errback, |
178 | callbackArgs=[pdu, redacted], | |
172 | callbackArgs=[pdu], | |
179 | 173 | errbackArgs=[pdu], |
180 | 174 | ) |
181 | 175 | |
182 | 176 | return deferreds |
177 | ||
178 | ||
179 | class PduToCheckSig(namedtuple("PduToCheckSig", [ | |
180 | "pdu", "redacted_pdu_json", "event_id_domain", "sender_domain", "deferreds", | |
181 | ])): | |
182 | pass | |
183 | ||
184 | ||
185 | def _check_sigs_on_pdus(keyring, pdus): | |
186 | """Check that the given events are correctly signed | |
187 | ||
188 | Args: | |
189 | keyring (synapse.crypto.Keyring): keyring object to do the checks | |
190 | pdus (Collection[EventBase]): the events to be checked | |
191 | ||
192 | Returns: | |
193 | List[Deferred]: a Deferred for each event in pdus, which will either succeed if | |
194 | the signatures are valid, or fail (with a SynapseError) if not. | |
195 | """ | |
196 | ||
197 | # (currently this is written assuming the v1 room structure; we'll probably want a | |
198 | # separate function for checking v2 rooms) | |
199 | ||
200 | # we want to check that the event is signed by: | |
201 | # | |
202 | # (a) the server which created the event_id | |
203 | # | |
204 | # (b) the sender's server. | |
205 | # | |
206 | # - except in the case of invites created from a 3pid invite, which are exempt | |
207 | # from this check, because the sender has to match that of the original 3pid | |
208 | # invite, but the event may come from a different HS, for reasons that I don't | |
209 | # entirely grok (why do the senders have to match? and if they do, why doesn't the | |
210 | # joining server ask the inviting server to do the switcheroo with | |
211 | # exchange_third_party_invite?). | |
212 | # | |
213 | # That's pretty awful, since redacting such an invite will render it invalid | |
214 | # (because it will then look like a regular invite without a valid signature), | |
215 | # and signatures are *supposed* to be valid whether or not an event has been | |
216 | # redacted. But this isn't the worst of the ways that 3pid invites are broken. | |
217 | # | |
218 | # let's start by getting the domain for each pdu, and flattening the event back | |
219 | # to JSON. | |
220 | pdus_to_check = [ | |
221 | PduToCheckSig( | |
222 | pdu=p, | |
223 | redacted_pdu_json=prune_event(p).get_pdu_json(), | |
224 | event_id_domain=get_domain_from_id(p.event_id), | |
225 | sender_domain=get_domain_from_id(p.sender), | |
226 | deferreds=[], | |
227 | ) | |
228 | for p in pdus | |
229 | ] | |
230 | ||
231 | # first make sure that the event is signed by the event_id's domain | |
232 | deferreds = keyring.verify_json_objects_for_server([ | |
233 | (p.event_id_domain, p.redacted_pdu_json) | |
234 | for p in pdus_to_check | |
235 | ]) | |
236 | ||
237 | for p, d in zip(pdus_to_check, deferreds): | |
238 | p.deferreds.append(d) | |
239 | ||
240 | # now let's look for events where the sender's domain is different to the | |
241 | # event id's domain (normally only the case for joins/leaves), and add additional | |
242 | # checks. | |
243 | pdus_to_check_sender = [ | |
244 | p for p in pdus_to_check | |
245 | if p.sender_domain != p.event_id_domain and not _is_invite_via_3pid(p.pdu) | |
246 | ] | |
247 | ||
248 | more_deferreds = keyring.verify_json_objects_for_server([ | |
249 | (p.sender_domain, p.redacted_pdu_json) | |
250 | for p in pdus_to_check_sender | |
251 | ]) | |
252 | ||
253 | for p, d in zip(pdus_to_check_sender, more_deferreds): | |
254 | p.deferreds.append(d) | |
255 | ||
256 | # replace lists of deferreds with single Deferreds | |
257 | return [_flatten_deferred_list(p.deferreds) for p in pdus_to_check] | |
258 | ||
259 | ||
260 | def _flatten_deferred_list(deferreds): | |
261 | """Given a list of one or more deferreds, either return the single deferred, or | |
262 | combine into a DeferredList. | |
263 | """ | |
264 | if len(deferreds) > 1: | |
265 | return DeferredList(deferreds, fireOnOneErrback=True, consumeErrors=True) | |
266 | else: | |
267 | assert len(deferreds) == 1 | |
268 | return deferreds[0] | |
269 | ||
270 | ||
271 | def _is_invite_via_3pid(event): | |
272 | return ( | |
273 | event.type == EventTypes.Member | |
274 | and event.membership == Membership.INVITE | |
275 | and "third_party_invite" in event.content | |
276 | ) | |
183 | 277 | |
184 | 278 | |
185 | 279 | def event_from_pdu_json(pdu_json, outlier=False): |
98 | 98 | |
99 | 99 | @defer.inlineCallbacks |
100 | 100 | @log_function |
101 | def on_incoming_transaction(self, transaction_data): | |
101 | def on_incoming_transaction(self, origin, transaction_data): | |
102 | 102 | # keep this as early as possible to make the calculated origin ts as |
103 | 103 | # accurate as possible. |
104 | 104 | request_time = self._clock.time_msec() |
107 | 107 | |
108 | 108 | if not transaction.transaction_id: |
109 | 109 | raise Exception("Transaction missing transaction_id") |
110 | if not transaction.origin: | |
111 | raise Exception("Transaction missing origin") | |
112 | 110 | |
113 | 111 | logger.debug("[%s] Got transaction", transaction.transaction_id) |
114 | 112 | |
115 | 113 | # use a linearizer to ensure that we don't process the same transaction |
116 | 114 | # multiple times in parallel. |
117 | 115 | with (yield self._transaction_linearizer.queue( |
118 | (transaction.origin, transaction.transaction_id), | |
116 | (origin, transaction.transaction_id), | |
119 | 117 | )): |
120 | 118 | result = yield self._handle_incoming_transaction( |
121 | transaction, request_time, | |
119 | origin, transaction, request_time, | |
122 | 120 | ) |
123 | 121 | |
124 | 122 | defer.returnValue(result) |
125 | 123 | |
126 | 124 | @defer.inlineCallbacks |
127 | def _handle_incoming_transaction(self, transaction, request_time): | |
125 | def _handle_incoming_transaction(self, origin, transaction, request_time): | |
128 | 126 | """ Process an incoming transaction and return the HTTP response |
129 | 127 | |
130 | 128 | Args: |
129 | origin (unicode): the server making the request | |
131 | 130 | transaction (Transaction): incoming transaction |
132 | 131 | request_time (int): timestamp that the HTTP request arrived at |
133 | 132 | |
134 | 133 | Returns: |
135 | 134 | Deferred[(int, object)]: http response code and body |
136 | 135 | """ |
137 | response = yield self.transaction_actions.have_responded(transaction) | |
136 | response = yield self.transaction_actions.have_responded(origin, transaction) | |
138 | 137 | |
139 | 138 | if response: |
140 | 139 | logger.debug( |
148 | 147 | |
149 | 148 | received_pdus_counter.inc(len(transaction.pdus)) |
150 | 149 | |
151 | origin_host, _ = parse_server_name(transaction.origin) | |
150 | origin_host, _ = parse_server_name(origin) | |
152 | 151 | |
153 | 152 | pdus_by_room = {} |
154 | 153 | |
189 | 188 | event_id = pdu.event_id |
190 | 189 | try: |
191 | 190 | yield self._handle_received_pdu( |
192 | transaction.origin, pdu | |
191 | origin, pdu | |
193 | 192 | ) |
194 | 193 | pdu_results[event_id] = {} |
195 | 194 | except FederationError as e: |
211 | 210 | if hasattr(transaction, "edus"): |
212 | 211 | for edu in (Edu(**x) for x in transaction.edus): |
213 | 212 | yield self.received_edu( |
214 | transaction.origin, | |
213 | origin, | |
215 | 214 | edu.edu_type, |
216 | 215 | edu.content |
217 | 216 | ) |
223 | 222 | logger.debug("Returning: %s", str(response)) |
224 | 223 | |
225 | 224 | yield self.transaction_actions.set_response( |
225 | origin, | |
226 | 226 | transaction, |
227 | 227 | 200, response |
228 | 228 | ) |
35 | 35 | self.store = datastore |
36 | 36 | |
37 | 37 | @log_function |
38 | def have_responded(self, transaction): | |
38 | def have_responded(self, origin, transaction): | |
39 | 39 | """ Have we already responded to a transaction with the same id and |
40 | 40 | origin? |
41 | 41 | |
49 | 49 | "transaction_id") |
50 | 50 | |
51 | 51 | return self.store.get_received_txn_response( |
52 | transaction.transaction_id, transaction.origin | |
52 | transaction.transaction_id, origin | |
53 | 53 | ) |
54 | 54 | |
55 | 55 | @log_function |
56 | def set_response(self, transaction, code, response): | |
56 | def set_response(self, origin, transaction, code, response): | |
57 | 57 | """ Persist how we responded to a transaction. |
58 | 58 | |
59 | 59 | Returns: |
65 | 65 | |
66 | 66 | return self.store.set_received_txn_response( |
67 | 67 | transaction.transaction_id, |
68 | transaction.origin, | |
68 | origin, | |
69 | 69 | code, |
70 | 70 | response, |
71 | 71 | ) |
352 | 352 | |
353 | 353 | try: |
354 | 354 | code, response = yield self.handler.on_incoming_transaction( |
355 | transaction_data | |
355 | origin, transaction_data, | |
356 | 356 | ) |
357 | 357 | except Exception: |
358 | 358 | logger.exception("on_incoming_transaction failed") |
32 | 32 | ) |
33 | 33 | |
34 | 34 | |
35 | def _expect_edu(destination, edu_type, content, origin="test"): | |
35 | def _expect_edu_transaction(edu_type, content, origin="test"): | |
36 | 36 | return { |
37 | 37 | "origin": origin, |
38 | 38 | "origin_server_ts": 1000000, |
41 | 41 | } |
42 | 42 | |
43 | 43 | |
44 | def _make_edu_json(origin, edu_type, content): | |
45 | return json.dumps(_expect_edu("test", edu_type, content, origin=origin)).encode( | |
44 | def _make_edu_transaction_json(edu_type, content): | |
45 | return json.dumps(_expect_edu_transaction(edu_type, content)).encode( | |
46 | 46 | 'utf8' |
47 | 47 | ) |
48 | 48 | |
189 | 189 | call( |
190 | 190 | "farm", |
191 | 191 | path="/_matrix/federation/v1/send/1000000/", |
192 | data=_expect_edu( | |
193 | "farm", | |
192 | data=_expect_edu_transaction( | |
194 | 193 | "m.typing", |
195 | 194 | content={ |
196 | 195 | "room_id": self.room_id, |
220 | 219 | |
221 | 220 | self.assertEquals(self.event_source.get_current_key(), 0) |
222 | 221 | |
223 | yield self.mock_federation_resource.trigger( | |
222 | (code, response) = yield self.mock_federation_resource.trigger( | |
224 | 223 | "PUT", |
225 | 224 | "/_matrix/federation/v1/send/1000000/", |
226 | _make_edu_json( | |
227 | "farm", | |
225 | _make_edu_transaction_json( | |
228 | 226 | "m.typing", |
229 | 227 | content={ |
230 | 228 | "room_id": self.room_id, |
232 | 230 | "typing": True, |
233 | 231 | }, |
234 | 232 | ), |
235 | federation_auth=True, | |
233 | federation_auth_origin=b'farm', | |
236 | 234 | ) |
237 | 235 | |
238 | 236 | self.on_new_event.assert_has_calls( |
263 | 261 | call( |
264 | 262 | "farm", |
265 | 263 | path="/_matrix/federation/v1/send/1000000/", |
266 | data=_expect_edu( | |
267 | "farm", | |
264 | data=_expect_edu_transaction( | |
268 | 265 | "m.typing", |
269 | 266 | content={ |
270 | 267 | "room_id": self.room_id, |
305 | 305 | |
306 | 306 | @patch('twisted.web.http.Request') |
307 | 307 | @defer.inlineCallbacks |
308 | def trigger(self, http_method, path, content, mock_request, federation_auth=False): | |
308 | def trigger( | |
309 | self, http_method, path, content, mock_request, | |
310 | federation_auth_origin=None, | |
311 | ): | |
309 | 312 | """ Fire an HTTP event. |
310 | 313 | |
311 | 314 | Args: |
314 | 317 | content : The HTTP body |
315 | 318 | mock_request : Mocked request to pass to the event so it can get |
316 | 319 | content. |
320 | federation_auth_origin (bytes|None): domain to authenticate as, for federation | |
317 | 321 | Returns: |
318 | 322 | A tuple of (code, response) |
319 | 323 | Raises: |
334 | 338 | mock_request.getClientIP.return_value = "-" |
335 | 339 | |
336 | 340 | headers = {} |
337 | if federation_auth: | |
338 | headers[b"Authorization"] = [b"X-Matrix origin=test,key=,sig="] | |
341 | if federation_auth_origin is not None: | |
342 | headers[b"Authorization"] = [ | |
343 | b"X-Matrix origin=%s,key=,sig=" % (federation_auth_origin, ) | |
344 | ] | |
339 | 345 | mock_request.requestHeaders.getRawHeaders = mock_getRawHeaders(headers) |
340 | 346 | |
341 | 347 | # return the right path if the event requires it |