Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-05-23 - Secure File Permissions
**Vulnerability:** Sensitive files (`cookies.jar`, `settings.json`, `log.txt`, `dump.dat`) were created with default umask permissions (often `0o664`), allowing other users on the system to read potentially sensitive data (like session tokens) or modify them (RCE risk with `pickle` in `cookies.jar`).
**Learning:** `aiohttp.CookieJar` uses `pickle` for serialization by default, which is an RCE vector if the file is writable by others. Default file creation in Python follows the system umask, which is often permissive.
**Prevention:** Explicitly restrict file permissions to `0o600` (Read/Write by Owner only) for sensitive files immediately after creation or before access. Use `os.chmod` on POSIX systems.
3 changes: 2 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from settings import Settings
from version import __version__
from exceptions import CaptchaRequired
from utils import lock_file, resource_path, set_root_icon
from utils import lock_file, resource_path, set_root_icon, set_secure_permissions
from constants import LOGGING_LEVELS, SELF_PATH, FILE_FORMATTER, LOG_PATH, LOCK_PATH

if TYPE_CHECKING:
Expand Down Expand Up @@ -146,6 +146,7 @@ async def main():
logger.setLevel(settings.logging_level)
if settings.log:
handler = logging.FileHandler(LOG_PATH)
set_secure_permissions(LOG_PATH)
handler.setFormatter(FILE_FORMATTER)
logger.addHandler(handler)
logging.getLogger("TwitchDrops.gql").setLevel(settings.debug_gql)
Expand Down
4 changes: 4 additions & 0 deletions twitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
RateLimiter,
AwaitableValue,
ExponentialBackoff,
set_secure_permissions,
)
from constants import (
CALL,
Expand Down Expand Up @@ -461,6 +462,7 @@ async def get_session(self) -> aiohttp.ClientSession:
cookie_jar = aiohttp.CookieJar()
try:
if COOKIES_PATH.exists():
set_secure_permissions(COOKIES_PATH)
cookie_jar.load(COOKIES_PATH)
except Exception:
# if loading in the cookies file ends up in an error, just ignore it
Expand Down Expand Up @@ -507,6 +509,7 @@ async def shutdown(self) -> None:
if not cookie:
del cookie_jar._cookies[cookie_key]
cookie_jar.save(COOKIES_PATH)
set_secure_permissions(COOKIES_PATH)
await self._session.close()
self._session = None
self._drops.clear()
Expand Down Expand Up @@ -588,6 +591,7 @@ async def run(self):
with open(DUMP_PATH, 'w', encoding="utf8"):
# replace the existing file with an empty one
pass
set_secure_permissions(DUMP_PATH)
while True:
try:
await self._run()
Expand Down
12 changes: 12 additions & 0 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,21 @@ def json_load(path: Path, defaults: _JSON_T, *, merge: bool = True) -> _JSON_T:
return cast(_JSON_T, combined)


def set_secure_permissions(path: Path) -> None:
"""
Sets file permissions to 600 (read/write by owner only) on POSIX systems.
"""
if os.name == "posix":
try:
path.chmod(0o600)
except OSError:
logger.warning(f"Failed to set secure permissions on {path}")


def json_save(path: Path, contents: Mapping[Any, Any], *, sort: bool = False) -> None:
with open(path, 'w', encoding="utf8") as file:
json.dump(contents, file, default=_serialize, sort_keys=sort, indent=4)
set_secure_permissions(path)


def webopen(url: URL | str):
Expand Down