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
24 changes: 22 additions & 2 deletions censys/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,25 @@ def get_config_path() -> str:
return CONFIG_PATH


def _restricted_opener(path: str, flags: int) -> int:
"""Opener that creates files readable and writable by the owner only.

Args:
path (str): Path to open.
flags (int): Flags passed by `open()`.

Returns:
int: File descriptor.
"""
return os.open(path, flags, 0o600)


def write_config(config: configparser.ConfigParser) -> None:
"""Writes config to file.

The config file contains API credentials, so the directory and file are
restricted to the owner (0700/0600) rather than inheriting the umask.

Args:
config (configparser.ConfigParser): Configuration to write.

Expand All @@ -45,8 +61,12 @@ def write_config(config: configparser.ConfigParser) -> None:
"Cannot write to home directory. Please set the `CENSYS_CONFIG_PATH` environmental variable to a writeable location."
)
elif not os.path.isdir(CENSYS_PATH):
os.makedirs(CENSYS_PATH)
with open(config_path, "w") as configfile:
os.makedirs(CENSYS_PATH, mode=0o700)
else:
os.chmod(CENSYS_PATH, 0o700)
if os.path.isfile(config_path):
os.chmod(config_path, 0o600)
with open(config_path, "w", opener=_restricted_opener) as configfile:
config.write(configfile)


Expand Down
40 changes: 37 additions & 3 deletions tests/cli/test_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os
import stat

import pytest
import responses

Expand All @@ -9,8 +12,10 @@
CENSYS_PATH,
CONFIG_PATH,
DEFAULT,
_restricted_opener,
default_config,
get_config,
write_config,
)

TEST_CONFIG_PATH = CONFIG_PATH + ".test"
Expand Down Expand Up @@ -45,6 +50,7 @@ def setUp(self):
)
self.mocker.patch("rich.prompt.Prompt.ask", side_effect=prompt_side_effect)
self.mocker.patch("rich.prompt.Confirm.ask", side_effect=confirm_side_effect)
self.mock_chmod = self.mocker.patch("censys.common.config.os.chmod")

def test_search_config(self):
# Mock
Expand All @@ -65,7 +71,9 @@ def test_search_config(self):
cli_main()

# Assert that the config file was read from the right place
self.mock_open.assert_called_with(TEST_CONFIG_PATH, "w")
self.mock_open.assert_called_with(
TEST_CONFIG_PATH, "w", opener=_restricted_opener
)

def test_search_config_failed(self):
# Mock
Expand Down Expand Up @@ -106,7 +114,7 @@ def test_search_config_makedirs(self):
with pytest.raises(SystemExit, match="0"):
cli_main()

mock_makedirs.assert_called_with(CENSYS_PATH)
mock_makedirs.assert_called_with(CENSYS_PATH, mode=0o700)

def test_config_default(self):
mock_isfile = self.mocker.patch(
Expand Down Expand Up @@ -141,7 +149,7 @@ def test_search_config_custom_config(self):
cli_main()

# Assert that the config file was read from the right place
self.mock_open.assert_called_with("censys.cfg", "w")
self.mock_open.assert_called_with("censys.cfg", "w", opener=_restricted_opener)

def test_search_config_perm_error(self):
self.patch_args(
Expand All @@ -160,3 +168,29 @@ def test_search_config_perm_error(self):

with pytest.raises(SystemExit, match="1"):
cli_main()


@pytest.mark.skipif(os.name != "posix", reason="POSIX file permissions only")
def test_write_config_restricts_permissions(tmp_path, mocker, monkeypatch):
monkeypatch.delenv("CENSYS_CONFIG_PATH", raising=False)
censys_path = tmp_path / ".config" / "censys"
config_path = censys_path / "censys.cfg"
mocker.patch("censys.common.config.HOME_PATH", str(tmp_path))
mocker.patch("censys.common.config.CENSYS_PATH", str(censys_path))
mocker.patch("censys.common.config.CONFIG_PATH", str(config_path))
old_umask = os.umask(0o022)
try:
write_config(get_config())

assert stat.S_IMODE(os.stat(censys_path).st_mode) == 0o700
assert stat.S_IMODE(os.stat(config_path).st_mode) == 0o600

# Pre-existing loose permissions are tightened on rewrite
os.chmod(censys_path, 0o755)
os.chmod(config_path, 0o644)
write_config(get_config())

assert stat.S_IMODE(os.stat(censys_path).st_mode) == 0o700
assert stat.S_IMODE(os.stat(config_path).st_mode) == 0o600
finally:
os.umask(old_umask)
Loading