fix: refuse a token store directory owned by another user or left writable (SEC-13)
This commit is contained in:
1 parent
d96c3ac604
commit
6f7a3c0e5b
2 files changed
+252
-4
No files matched your search
@@ -13,7 +13,8 @@ What is stored:
|
||||
* sessions: the Gitea login and the user's Gitea access and refresh tokens;
|
||||
* our own access and refresh tokens, only as SHA-256 hashes.
|
||||
|
||||
File safety: the directory is created with mode 0700 and the file is written
|
||||
File safety: the directory is created with mode 0700 (an existing one must be owned by the
|
||||
running user and is tightened to 0700, otherwise startup stops) and the file is written
|
||||
with mode 0600 through a fresh ``O_EXCL`` temporary file and an atomic
|
||||
``os.replace``. A symlink at the store path is refused. A file that cannot be
|
||||
decrypted or parsed stops startup with a clear message instead of being wiped,
|
||||
@@ -132,16 +133,40 @@ class TokenRecord:
|
||||
|
||||
|
||||
def _restrict_directory(directory: Path) -> None:
|
||||
"""Create the state directory with mode 0700, or tighten an existing one we own."""
|
||||
"""Create the state directory with mode 0700, or check and tighten an existing one.
|
||||
|
||||
Whoever controls the directory can swap the store for an older copy (which
|
||||
resurrects revoked grants) or delete it, so an existing directory must belong
|
||||
to the effective user and must not stay writable by group or others.
|
||||
|
||||
Raises:
|
||||
OAuthStoreError: The directory is owned by another user or remains writable
|
||||
by group or others after an attempt to tighten it.
|
||||
"""
|
||||
if not directory.exists():
|
||||
directory.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
return
|
||||
mode = stat.S_IMODE(directory.stat().st_mode)
|
||||
info = os.stat(directory)
|
||||
if not stat.S_ISDIR(info.st_mode):
|
||||
raise OAuthStoreError(f"{directory} is not a directory")
|
||||
owner = getattr(os, "geteuid", lambda: info.st_uid)()
|
||||
if info.st_uid != owner:
|
||||
raise OAuthStoreError(
|
||||
f"{directory} is owned by another user (uid {info.st_uid}, this process runs as "
|
||||
f"uid {owner}); refusing to keep the OAuth token store there. Use a DATA_DIR "
|
||||
"owned by the service user"
|
||||
)
|
||||
mode = stat.S_IMODE(info.st_mode)
|
||||
if mode & 0o077:
|
||||
try:
|
||||
directory.chmod(0o700)
|
||||
except OSError:
|
||||
logger.warning("could not restrict the permissions of the state directory to 0700")
|
||||
if stat.S_IMODE(os.stat(directory).st_mode) & 0o022:
|
||||
raise OAuthStoreError(
|
||||
f"{directory} is writable by group or others and could not be restricted; "
|
||||
"refusing to keep the OAuth token store there. Run 'chmod 700' on it"
|
||||
)
|
||||
|
||||
|
||||
def _parse_document(document: Any) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
||||
@@ -191,10 +216,12 @@ class OAuthStore:
|
||||
"""Open the store, reading and decrypting the file when it exists.
|
||||
|
||||
Raises:
|
||||
OAuthStoreError: The path is a symlink or not a regular file, or the
|
||||
OAuthStoreError: The directory belongs to another user or stays group or
|
||||
world writable, the path is a symlink or not a regular file, or the
|
||||
file cannot be decrypted or parsed.
|
||||
"""
|
||||
store = cls(directory, secret, clock)
|
||||
_restrict_directory(directory)
|
||||
store._read()
|
||||
store.prune()
|
||||
return store
|
||||
|
||||
Reference in new issue
Block a user