From ae63faa8e546381ab16f18bdfeb66a9cee86c88b Mon Sep 17 00:00:00 2001 From: Konstantin Ryabitsev Date: Tue, 23 Aug 2022 13:22:22 -0400 Subject: send-receive: add some anti-spam protections The goal of this service is to accept and send patches, nothing else. It's not a replacement for an SMTP server, just a replacement for really terrible SMTP servers that mangle patches. So, add some anti-spam protections: - only accept email that looks like patches or cover letters - don't accept anything other than text/plain mail - require that one of the to/cc addresses matches a predefined list of recognized mailing lists Not a guarantee that this service won't get abused, but it's a start to make sure that it won't be quite as tasty of a target. Signed-off-by: Konstantin Ryabitsev --- misc/send-receive.py | 54 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/misc/send-receive.py b/misc/send-receive.py index c54e0ef..574eb10 100644 --- a/misc/send-receive.py +++ b/misc/send-receive.py @@ -42,12 +42,15 @@ Deet-doot-dot, I'm a bot https://korg.docs.kernel.org/ ''' -DEFAULT_CFG = ''' +DEFAULT_CFG = r''' [main] myname = Web Endpoint myurl = http://localhost:8000/_b4_submit dburl = sqlite:///:memory: mydomains = kernel.org, linux.dev + # One of the To: or Cc: addrs must match this regex + # (to ensure that the message was intended to go to mailing lists) + mustdest = .*@(vger\.kernel\.org|lists\.linux\.dev|lists\.infradead\.org) dryrun = false [sendemail] smtpserver = localhost @@ -297,6 +300,10 @@ class SendReceiveListener(object): if not umsgs: self.send_error(resp, message='Missing the messages array') return + + diffre = re.compile(r'^(---.*\n\+\+\+|GIT binary patch|diff --git \w/\S+ \w/\S+)', flags=re.M | re.I) + diffstatre = re.compile(r'^\s*\d+ file.*\d+ (insertion|deletion)', flags=re.M | re.I) + msgs = list() conn = self._engine.connect() md = sa.MetaData() @@ -310,6 +317,26 @@ class SendReceiveListener(object): self.send_error(resp, message=f'Signature validation failed for message {at}') return msg = email.message_from_string(umsg) + # Some quick sanity checking: + # - Subject has to start with [PATCH + # - Content-type may ONLY be text/plain + # - Has to include a diff or a diffstat + passes = True + if not msg.get('Subject', '').startswith('[PATCH '): + passes = False + if passes: + cte = msg.get_content_type() + if cte.lower() != 'text/plain': + passes = False + if passes: + payload = msg.get_payload() + if not (diffre.search(payload) or diffstatre.search(payload)): + passes = False + + if not passes: + self.send_error(resp, message='This service only accepts patches') + return + msg.add_header('X-Endpoint-Received', f'by {servicename} with auth_id={auth_id}') msgs.append(msg) @@ -354,21 +381,30 @@ class SendReceiveListener(object): else: msg.add_header('Reply-To', f'<{origpair[1]}>') - # Does the subject start with [PATCH? - if subject.startswith('[PATCH '): - body = msg.get_payload() - # Parse it as a message and see if we get a From: header - cmsg = email.message_from_string(body) - if cmsg.get('From') is None: - cmsg.add_header('From', origfrom) - msg.set_payload(cmsg.as_string(policy=emlpolicy, maxheaderlen=0), charset='utf-8') + body = msg.get_payload() + # Parse it as a message and see if we get a From: header + cmsg = email.message_from_string(body) + if cmsg.get('From') is None: + cmsg.add_header('From', origfrom) + msg.set_payload(cmsg.as_string(policy=emlpolicy, maxheaderlen=0), charset='utf-8') alldests = utils.getaddresses([str(x) for x in msg.get_all('to', [])]) alldests += utils.getaddresses([str(x) for x in msg.get_all('cc', [])]) + alwaysbcc = self._config['main'].get('alwaysbcc') if alwaysbcc: alldests += utils.getaddresses([alwaysbcc]) destaddrs = {x[1] for x in alldests} + mustdest = self._config['main'].get('mustdest') + if mustdest: + matched = False + for destaddr in destaddrs: + if re.search(mustdest, destaddr, flags=re.I): + matched = True + break + if not matched: + self.send_error(resp, message='Destinations must include a mailing list we recognize.') + return bdata = msg.as_string(policy=emlpolicy).encode() -- cgit v1.2.3