Skip to content
Merged
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
68 changes: 46 additions & 22 deletions nexuscli/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,11 @@ def delete(self, *args, **kwargs):
BASE_URL = "https://chat.deepseek.com"

CONFIG_DIR.mkdir(parents=True, exist_ok=True)
try:
CONFIG_DIR.chmod(0o700)
except Exception:
pass
if not CONFIG_DIR.is_symlink():
try:
CONFIG_DIR.chmod(0o700)
except Exception:
pass

# Persistent session (cookies preserved across API calls)
_session: Optional[curl_requests.Session] = None
Expand All @@ -80,13 +81,28 @@ def _session_cache_key(token: str, cookie: Optional[str] = None) -> str:
# ---------- Cache Helpers ----------

def _cache_path(session_id: str, account: str = "primary") -> str:
store_dir = os.path.join(os.path.expanduser("~/.nexuscli/session_store"), account)
if ".." in str(account) or str(account).startswith("/") or "\\" in str(account):
raise ValueError("Invalid account name")

base_store = os.path.realpath(os.path.expanduser("~/.nexuscli/session_store"))
safe_account = "".join(c if c.isalnum() or c in "-_." else "_" for c in str(account))
store_dir = os.path.join(base_store, safe_account)

store_real = os.path.realpath(store_dir)
if os.path.commonpath([base_store, store_real]) != base_store:
raise ValueError("Invalid account path")

os.makedirs(store_dir, exist_ok=True)
try:
os.chmod(os.path.dirname(store_dir), 0o700)
os.chmod(store_dir, 0o700)
except Exception:
pass

# Restrict permissions of store_dir and parent directories if not symlinks
parent_store = os.path.dirname(store_dir)
for d_path in [parent_store, store_dir]:
p = Path(d_path)
if p.exists() and not p.is_symlink():
try:
p.chmod(0o700)
except Exception:
pass

if ".." in str(session_id) or str(session_id).startswith("/") or "\\" in str(session_id):
raise ValueError("Invalid file path")
Expand Down Expand Up @@ -114,16 +130,23 @@ def _cache_load(session_id: str, account: str = "primary") -> Optional[List[Dict
def _cache_save(session_id: str, messages: List[Dict[str, Any]], account: str = "primary"):
path = _cache_path(session_id, account)
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
os.chmod(os.path.dirname(path), 0o700)
except Exception:
pass

p_dir = Path(os.path.dirname(path))
if p_dir.exists() and not p_dir.is_symlink():
try:
p_dir.chmod(0o700)
except Exception:
pass

with open(path, "w") as f:
json.dump(messages, f, indent=2)
try:
os.chmod(path, 0o600)
except Exception:
pass

p_file = Path(path)
if p_file.exists() and not p_file.is_symlink():
try:
p_file.chmod(0o600)
except Exception:
pass


# ---------- Config Helpers ----------
Expand All @@ -136,10 +159,11 @@ def load_config() -> Dict[str, Any]:

def save_config(cfg: Dict[str, Any]):
CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
try:
CONFIG_FILE.chmod(0o600)
except Exception:
pass
if CONFIG_FILE.exists() and not CONFIG_FILE.is_symlink():
try:
CONFIG_FILE.chmod(0o600)
except Exception:
pass


def get_token() -> str:
Expand Down
91 changes: 91 additions & 0 deletions tests/test_nexuscli_privileges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import os
import sys
import shutil
from pathlib import Path
import pytest

def test_nexuscli_privileges_enforcement(tmp_path, monkeypatch):
test_config_dir = tmp_path / ".nexuscli"
test_config_file = test_config_dir / "config.json"

import nexuscli.core.api as nc
monkeypatch.setattr(nc, "CONFIG_DIR", test_config_dir)
monkeypatch.setattr(nc, "CONFIG_FILE", test_config_file)

test_config_dir.mkdir(parents=True, exist_ok=True)
if not test_config_dir.is_symlink():
try:
test_config_dir.chmod(0o700)
except Exception:
pass

if os.name != "nt":
assert (test_config_dir.stat().st_mode & 0o777) == 0o700

nc.save_config({"token": "secret_token"})
assert test_config_file.exists()
if os.name != "nt":
assert (test_config_file.stat().st_mode & 0o777) == 0o600

session_id = "test_session_uuid"
messages = [{"role": "user", "content": "hello"}]

monkeypatch.setattr(os.path, "expanduser", lambda path: path.replace("~", str(tmp_path)))

cache_path_str = nc._cache_path(session_id, account="test_account")
cache_path = Path(cache_path_str)

assert cache_path.parent.exists()
if os.name != "nt":
assert (cache_path.parent.stat().st_mode & 0o777) == 0o700
assert (cache_path.parent.parent.stat().st_mode & 0o777) == 0o700

nc._cache_save(session_id, messages, account="test_account")
assert cache_path.exists()
if os.name != "nt":
assert (cache_path.stat().st_mode & 0o777) == 0o600


def test_nexuscli_privileges_symlink_safety(tmp_path, monkeypatch):
import nexuscli.core.api as nc

target_file = tmp_path / "target_file.txt"
target_file.write_text("sensitvedata")
if os.name != "nt":
target_file.chmod(0o644)

symlink_file = tmp_path / "symlink_file.json"
symlink_file.symlink_to(target_file)

monkeypatch.setattr(nc, "CONFIG_FILE", symlink_file)
nc.save_config({"token": "some_token"})

if os.name != "nt":
assert (target_file.stat().st_mode & 0o777) == 0o644


def test_nexuscli_privileges_path_traversal_prevention(tmp_path, monkeypatch):
import nexuscli.core.api as nc

monkeypatch.setattr(os.path, "expanduser", lambda path: path.replace("~", str(tmp_path)))

with pytest.raises(ValueError):
nc._cache_path("../../../etc/passwd")

with pytest.raises(ValueError):
nc._cache_path("/absolute/path/traversal")

safe_path_str = nc._cache_path("session-123_abc.dot")
assert "session-123_abc.dot.json" in safe_path_str

unsanitized_path_str = nc._cache_path("session$#*!123")
assert "session____123.json" in unsanitized_path_str

with pytest.raises(ValueError):
nc._cache_path("session1", account="../../etc")

with pytest.raises(ValueError):
nc._cache_path("session1", account="/absolute/account")

acc_path_str = nc._cache_path("session1", account="acc$#*!123")
assert "acc____123" in acc_path_str
Loading