-
Notifications
You must be signed in to change notification settings - Fork 46
ztp: harden curl command construction in Downloader and DHCP hook #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -103,29 +103,29 @@ fi | |
|
|
||
| if [ "$(ztp status -c)" != "0:DISABLED" ]; then | ||
| if [ -n "$new_bootfile_name" ]; then | ||
| take_lock dhcp && echo $new_bootfile_name > $ZTP_JSON_URL_FILE | ||
| take_lock dhcp && echo "$new_bootfile_name" > $ZTP_JSON_URL_FILE | ||
| if [ -n "$new_tftp_server_name" ]; then | ||
| take_lock dhcp && echo $new_tftp_server_name > $ZTP_TFTP_SERVER_FILE | ||
| take_lock dhcp && printf '%s\n' "$new_tftp_server_name" > $ZTP_TFTP_SERVER_FILE | ||
| fi | ||
| fi | ||
|
|
||
| if [ -n "$new_dhcp6_boot_file_url" ]; then | ||
| take_lock dhcp6 && echo $new_dhcp6_boot_file_url > $ZTP_JSON_URL6_FILE | ||
| take_lock dhcp6 && printf '%s\n' "$new_dhcp6_boot_file_url" > $ZTP_JSON_URL6_FILE | ||
| fi | ||
|
Comment on lines
112
to
114
|
||
|
|
||
| if [ -n "$new_provisioning_script_url" ]; then | ||
| take_lock dhcp && echo $new_provisioning_script_url > $PROVISIONING_SCRIPT_URL_FILE | ||
| take_lock dhcp && printf '%s\n' "$new_provisioning_script_url" > $PROVISIONING_SCRIPT_URL_FILE | ||
| fi | ||
|
Comment on lines
116
to
118
|
||
|
|
||
| if [ -n "$new_dhcp6_provisioning_script_url" ]; then | ||
| take_lock dhcp6 && echo $new_dhcp6_provisioning_script_url > $PROVISIONING_SCRIPT_URL6_FILE | ||
| take_lock dhcp6 && printf '%s\n' "$new_dhcp6_provisioning_script_url" > $PROVISIONING_SCRIPT_URL6_FILE | ||
| fi | ||
|
Comment on lines
120
to
122
|
||
|
|
||
| if [ -n "$new_minigraph_url" ]; then | ||
| take_lock dhcp && echo $new_minigraph_url > ${GRAPH_URL} | ||
| take_lock dhcp && printf '%s\n' "$new_minigraph_url" > ${GRAPH_URL} | ||
|
|
||
| if [ -n "$new_acl_url" ]; then | ||
| take_lock dhcp && echo $new_acl_url > ${ACL_URL} | ||
| take_lock dhcp && printf '%s\n' "$new_acl_url" > ${ACL_URL} | ||
| fi | ||
|
Comment on lines
124
to
129
|
||
| fi | ||
| fi | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -696,6 +696,10 @@ def __downloadURL(self, url_file, dst_file, url_prefix=None): | |
| url_str = f.readline().strip() | ||
| f.close() | ||
|
|
||
| if ' ' in url_str or '\t' in url_str: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why should spaces be blocked?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. URLs containing literal spaces are invalid per RFC 3986 — legitimate URLs use percent-encoding ( Concretely, if a space were passed through, it could split into multiple arguments when processed, potentially injecting curl flags (e.g. |
||
| logger.error('Failed to download provided URL %s, URL contains whitespace.' % (url_str)) | ||
| return False | ||
|
|
||
| res = urlparse(url_str) | ||
| if res is None or res.scheme == '': | ||
| # Use passed url_prefix to construct final URL | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| """ | ||
| conftest.py — test environment setup for sonic-ztp unit tests. | ||
|
|
||
| Creates temporary directories to replace SONiC-specific system paths | ||
| (/host/ztp, /etc/rsyslog.d) so tests can run on a bare host without | ||
| a SONiC container. | ||
| """ | ||
|
|
||
| import os | ||
| import sys | ||
| import tempfile | ||
| import pytest | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Bootstrap: patch system paths BEFORE any ztp module is imported. | ||
| # ZTPCfg.py and Logger.py run module-level code at import time that touches | ||
| # /host/ztp and /etc/rsyslog.d, so the patches must be in place first. | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| # Create a single persistent temp dir for the whole test session. | ||
| _tmp_root = tempfile.mkdtemp(prefix="ztp_test_") | ||
| _fake_host_ztp = os.path.join(_tmp_root, "host", "ztp") | ||
| _fake_rsyslog_d = os.path.join(_tmp_root, "etc", "rsyslog.d") | ||
| _fake_sonic_dir = os.path.join(_tmp_root, "etc", "sonic") | ||
|
|
||
| os.makedirs(_fake_host_ztp, exist_ok=True) | ||
| os.makedirs(_fake_rsyslog_d, exist_ok=True) | ||
| os.makedirs(_fake_sonic_dir, exist_ok=True) | ||
|
|
||
| # Add ztp package to path so `from ztp.X import Y` works. | ||
| _ztp_pkg_dir = os.path.join( | ||
| os.path.dirname(__file__), | ||
| "..", "src", "usr", "lib", "python3", "dist-packages" | ||
| ) | ||
| if _ztp_pkg_dir not in sys.path: | ||
| sys.path.insert(0, os.path.abspath(_ztp_pkg_dir)) | ||
|
|
||
| # Patch defaults BEFORE importing any ztp submodule. | ||
| import ztp.defaults as _defaults | ||
|
|
||
|
Comment on lines
+14
to
+40
|
||
| _defaults.cfg_file = os.path.join(_fake_host_ztp, "ztp_cfg.json") | ||
| _defaults.defaultCfg["ztp-cfg-dir"] = _fake_host_ztp | ||
| _defaults.defaultCfg["ztp-json"] = os.path.join(_fake_host_ztp, "ztp_data.json") | ||
| _defaults.defaultCfg["ztp-json-shadow"] = os.path.join(_fake_host_ztp, "ztp_data_shadow.json") | ||
| _defaults.defaultCfg["ztp-json-local"] = os.path.join(_fake_host_ztp, "ztp_data_local.json") | ||
| _defaults.defaultCfg["provisioning-script"] = os.path.join(_fake_host_ztp, "provisioning-script") | ||
| _defaults.defaultCfg["rsyslog-ztp-log-file-conf"] = os.path.join(_fake_rsyslog_d, "10-ztp-log-file.conf") | ||
| _defaults.defaultCfg["rsyslog-ztp-consile-log-file-conf"] = os.path.join(_fake_rsyslog_d, "10-ztp-console-logging.conf") | ||
| _defaults.defaultCfg["log-file"] = os.path.join(_tmp_root, "ztp.log") | ||
| _defaults.defaultCfg["ztp-tmp"] = os.path.join(_tmp_root, "tmp") | ||
|
|
||
| os.makedirs(_defaults.defaultCfg["ztp-tmp"], exist_ok=True) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,186 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ''' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Input validation tests for curl command construction in Downloader.py | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Tests verify that url, dst_file, and curl_args are handled correctly | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| when DHCP-supplied values are used. runCommand is mocked so no real curl | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| or network is required. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ''' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from unittest.mock import patch, MagicMock | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from ztp.Downloader import Downloader | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Helper | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # --------------------------------------------------------------------------- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def _make_downloader(**kwargs): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Return a Downloader with safe defaults for unit testing.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Downloader( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+11
to
+23
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import pytest | |
| from unittest.mock import patch, MagicMock | |
| from ztp.Downloader import Downloader | |
| # --------------------------------------------------------------------------- | |
| # Helper | |
| # --------------------------------------------------------------------------- | |
| def _make_downloader(**kwargs): | |
| """Return a Downloader with safe defaults for unit testing.""" | |
| return Downloader( | |
| import importlib | |
| import pytest | |
| from unittest.mock import patch, MagicMock | |
| # --------------------------------------------------------------------------- | |
| # Helper | |
| # --------------------------------------------------------------------------- | |
| def _get_downloader_class(): | |
| """ | |
| Import Downloader only after stubbing sysEeprom so unit tests do not | |
| execute decode-syseeprom on the host at import time. | |
| """ | |
| import ztp.DecodeSysEeprom | |
| fake_sys_eeprom = MagicMock() | |
| fake_sys_eeprom.getSystemEEPROM.return_value = {} | |
| with patch.object( | |
| ztp.DecodeSysEeprom, | |
| 'sysEeprom', | |
| return_value=fake_sys_eeprom, | |
| ): | |
| downloader_module = importlib.import_module('ztp.Downloader') | |
| return downloader_module.Downloader | |
| def _make_downloader(**kwargs): | |
| """Return a Downloader with safe defaults for unit testing.""" | |
| downloader_cls = _get_downloader_class() | |
| return downloader_cls( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
echowith DHCP-supplied values can be unsafe/ambiguous when the value begins with-nor contains backslash escape sequences (echo behavior varies). For robust writing of untrusted strings, preferprintf '%s\n' "$new_bootfile_name"(and likewise for other DHCP option values).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — all 6 DHCP-supplied values now use printf '%s\n' instead of echo.