Skip to content

Commit 8e43dba

Browse files
humitosstsewd
andauthored
Build: obfuscate private variables (#12366)
Replace the value of private variables with the first 4 chars of it and `****`. This is a protection to avoid leaking private variables by mistake. The same pattern is used in the dashboard when listing these variables. Closes readthedocs/readthedocs-corporate#2004 --------- Co-authored-by: Santos Gallegos <[email protected]>
1 parent 1cb6aa8 commit 8e43dba

File tree

2 files changed

+44
-2
lines changed

2 files changed

+44
-2
lines changed

readthedocs/doc_builder/environments.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ def sanitize_output(self, output: str) -> str:
200200
2. Chunk at around ``DATA_UPLOAD_MAX_MEMORY_SIZE`` bytes to be sent
201201
over the API call request
202202
203+
3. Obfuscate private environment variables.
204+
203205
:param output: stdout/stderr to be sanitized
204206
205207
:returns: sanitized output as string
@@ -232,6 +234,17 @@ def sanitize_output(self, output: str) -> str:
232234
f"{truncated_output}"
233235
)
234236

237+
# Obfuscate private environment variables.
238+
if self.build_env:
239+
# NOTE: we can't use `self._environment` here because we don't know
240+
# which variable is public/private since it's just a name/value
241+
# dictionary. We need to check with the APIProject object (`self.build_env.project`).
242+
for name, spec in self.build_env.project._environment_variables.items():
243+
if not spec["public"]:
244+
value = spec["value"]
245+
obfuscated_value = f"{value[:4]}****"
246+
sanitized = sanitized.replace(value, obfuscated_value)
247+
235248
return sanitized
236249

237250
def get_command(self):

readthedocs/rtd_tests/tests/test_doc_building.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django_dynamic_fixture import get
99
from docker.errors import APIError as DockerAPIError
1010

11+
from readthedocs.projects.models import APIProject
1112
from readthedocs.builds.models import Version
1213
from readthedocs.doc_builder.environments import (
1314
BuildCommand,
@@ -41,7 +42,7 @@ def test_record_command_as_success(self):
4142
api_client.command().patch.return_value = {
4243
"id": 1,
4344
}
44-
project = get(Project)
45+
project = APIProject(**get(Project).__dict__)
4546
build_env = LocalBuildEnvironment(
4647
project=project,
4748
build={
@@ -258,7 +259,7 @@ def test_missing_command(self):
258259

259260
def test_output(self):
260261
"""Test output command."""
261-
project = get(Project)
262+
project = APIProject(**get(Project).__dict__)
262263
api_client = mock.MagicMock()
263264
build_env = LocalBuildEnvironment(
264265
project=project,
@@ -310,6 +311,34 @@ def test_sanitize_output(self):
310311
for output, sanitized in checks:
311312
self.assertEqual(cmd.sanitize_output(output), sanitized)
312313

314+
def test_obfuscate_output_private_variables(self):
315+
build_env = mock.MagicMock()
316+
build_env.project = mock.MagicMock()
317+
build_env.project._environment_variables = mock.MagicMock()
318+
build_env.project._environment_variables.items.return_value = [
319+
(
320+
"PUBLIC",
321+
{
322+
"public": True,
323+
"value": "public-value",
324+
},
325+
),
326+
(
327+
"PRIVATE",
328+
{
329+
"public": False,
330+
"value": "private-value",
331+
},
332+
),
333+
]
334+
cmd = BuildCommand(["/bin/bash", "-c", "echo"], build_env=build_env)
335+
checks = (
336+
("public-value", "public-value"),
337+
("private-value", "priv****"),
338+
)
339+
for output, sanitized in checks:
340+
self.assertEqual(cmd.sanitize_output(output), sanitized)
341+
313342
@patch("subprocess.Popen")
314343
def test_unicode_output(self, mock_subprocess):
315344
"""Unicode output from command."""

0 commit comments

Comments
 (0)