Skip to content

Commit 1c36d8a

Browse files
authored
Merge pull request #376 from kyb3r/development
Security update
2 parents 24aecdd + e3d0f3e commit 1c36d8a

File tree

5 files changed

+57
-9
lines changed

5 files changed

+57
-9
lines changed

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
66
This project mostly adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html);
77
however, insignificant breaking changes does not guarantee a major version bump, see the reasoning [here](https://github.com/kyb3r/modmail/issues/319).
88

9+
# v3.2.2
10+
11+
Security update!
12+
13+
### Important
14+
15+
- Supporter permission users used to be able to "hack" snippets to reveal all your config vars, including your token and MongoURI.
16+
- Implemented some changes to address this bug:
17+
- All customizable variables used in snippets, close messages, etc, using the `{}` syntax, now forbids chaining 2 or more attributes and attributes that starts with `_`.
18+
- It is advised to update to this version.
19+
- If you felt your credentials have been leaked, consider changing your bot token / mongo uri.
20+
921
# v3.2.1
1022

1123
### Fixed

bot.py

+8-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "3.2.1"
1+
__version__ = "3.2.2"
22

33
import asyncio
44
import logging
@@ -33,7 +33,7 @@
3333
from core.clients import ApiClient, PluginDatabaseClient
3434
from core.config import ConfigManager
3535
from core.utils import human_join, strtobool, parse_alias
36-
from core.models import PermissionLevel, ModmailLogger
36+
from core.models import PermissionLevel, ModmailLogger, SafeFormatter
3737
from core.thread import ThreadManager
3838
from core.time import human_timedelta
3939

@@ -69,6 +69,7 @@ def __init__(self):
6969
self._session = None
7070
self._api = None
7171
self.metadata_loop = None
72+
self.formatter = SafeFormatter()
7273

7374
self._connected = asyncio.Event()
7475
self.start_time = datetime.utcnow()
@@ -120,7 +121,7 @@ def uptime(self) -> str:
120121
if days:
121122
fmt = "{d}d " + fmt
122123

123-
return fmt.format(d=days, h=hours, m=minutes, s=seconds)
124+
return self.formatter.format(fmt, d=days, h=hours, m=minutes, s=seconds)
124125

125126
def _configure_logging(self):
126127
level_text = self.config["log_level"].upper()
@@ -294,6 +295,8 @@ def guild_id(self) -> typing.Optional[int]:
294295
except ValueError:
295296
self.config.remove("guild_id")
296297
logger.critical("Invalid GUILD_ID set.")
298+
else:
299+
logger.debug("No GUILD_ID set.")
297300
return None
298301

299302
@property
@@ -456,7 +459,7 @@ async def on_ready(self):
456459
await self.wait_for_connected()
457460

458461
if self.guild is None:
459-
logger.debug("Logging out due to invalid GUILD_ID.")
462+
logger.error("Logging out due to invalid GUILD_ID.")
460463
return await self.logout()
461464

462465
logger.line()
@@ -835,7 +838,7 @@ async def process_commands(self, message):
835838
thread = await self.threads.find(channel=message.channel)
836839
snippet = self.snippets[cmd]
837840
if thread:
838-
snippet = snippet.format(recipient=thread.recipient)
841+
snippet = self.formatter.format(snippet, recipient=thread.recipient)
839842
message.content = f"{self.prefix}reply {snippet}"
840843

841844
ctxs = await self.get_contexts(message)

core/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def populate_cache(self) -> dict:
139139
)
140140
if os.path.exists(config_json):
141141
logger.debug("Loading envs from config.json.")
142-
with open(config_json, "r") as f:
142+
with open(config_json, "r", encoding="utf-8") as f:
143143
# Config json should override env vars
144144
try:
145145
data.update(

core/models.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import _string
12
import logging
23
from enum import IntEnum
4+
from string import Formatter
35

46
from discord import Color, Embed
57
from discord.ext import commands
@@ -85,3 +87,33 @@ class _Default:
8587

8688

8789
Default = _Default()
90+
91+
92+
class SafeFormatter(Formatter):
93+
94+
def get_field(self, field_name, args, kwargs):
95+
first, rest = _string.formatter_field_name_split(field_name)
96+
97+
try:
98+
obj = self.get_value(first, args, kwargs)
99+
except (IndexError, KeyError):
100+
return "<Invalid>", first
101+
102+
# loop through the rest of the field_name, doing
103+
# getattr or getitem as needed
104+
# stops when reaches the depth of 2 or starts with _.
105+
try:
106+
for n, (is_attr, i) in enumerate(rest):
107+
if n >= 2:
108+
break
109+
if is_attr:
110+
if str(i).startswith('_'):
111+
break
112+
obj = getattr(obj, i)
113+
else:
114+
obj = obj[i]
115+
else:
116+
return obj, first
117+
except (IndexError, KeyError):
118+
pass
119+
return "<Invalid>", first

core/thread.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -393,8 +393,8 @@ async def _close(
393393
else:
394394
message = self.bot.config["thread_close_response"]
395395

396-
message = message.format(
397-
closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None
396+
message = self.bot.formatter.format(
397+
message, closer=closer, loglink=log_url, logkey=log_data["key"] if log_data else None
398398
)
399399

400400
embed.description = message
@@ -492,7 +492,8 @@ async def _restart_close_timer(self):
492492
)
493493

494494
# Grab message
495-
close_message = self.bot.config["thread_auto_close_response"].format(
495+
close_message = self.bot.formatter.format(
496+
self.bot.config["thread_auto_close_response"],
496497
timeout=human_time
497498
)
498499

0 commit comments

Comments
 (0)