diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 609d0e39a9..8f78ec9074 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -26,6 +26,7 @@ * Fixed `SELECT *` output being corrupted when joined tables share column names. Duplicate column names are now disambiguated by appending a numeric suffix (e.g. `NAME`, `NAME_2`). * Fixed `snow connection generate-jwt` and `snow connection generate-workload-identity-token` failing with `Connection None is not configured` when used with `--temporary-connection`. * The internal connection cache now remembers failed connect attempts and re-raises the original exception on subsequent accesses within the same process, instead of re-dialing Snowflake every time a command accesses the shared connection. This fixes, among other cases, the customer-visible duplicate `LOGIN_HISTORY` events (and `OVERFLOW_FAILURE_EVENTS_ELIDED`) previously emitted when a `snow` invocation was rejected by an authentication policy. +* `--like` values passed to `snow object list` and `snow spcs image-repository list-images` are now escaped before being inlined into the generated `SHOW ... LIKE '...'` statement. Previously, a single quote in the pattern would terminate the literal and cause the rest of the argument to be parsed as SQL, so `--like "foo'; drop table users; --"` could execute arbitrary statements in the caller's own session. # v3.17.0 diff --git a/src/snowflake/cli/_plugins/object/manager.py b/src/snowflake/cli/_plugins/object/manager.py index cd92e9fe96..9f3271d67e 100644 --- a/src/snowflake/cli/_plugins/object/manager.py +++ b/src/snowflake/cli/_plugins/object/manager.py @@ -58,7 +58,8 @@ def show( query = " ".join(query_parts) if like: - query += f" like '{like}'" + escaped_like = like.replace("'", "''") + query += f" like '{escaped_like}'" if scope[0] is not None: scope_type = scope[0].replace("-", " ") if scope[1] is not None: diff --git a/src/snowflake/cli/_plugins/spcs/image_repository/manager.py b/src/snowflake/cli/_plugins/spcs/image_repository/manager.py index 60ed27d002..085175605c 100644 --- a/src/snowflake/cli/_plugins/spcs/image_repository/manager.py +++ b/src/snowflake/cli/_plugins/spcs/image_repository/manager.py @@ -85,7 +85,8 @@ def create( def list_images(self, repo_name: str, like_option: str) -> SnowflakeCursor: if like_option: - query = f"show images like '{like_option}' in image repository {repo_name}" + escaped_like = like_option.replace("'", "''") + query = f"show images like '{escaped_like}' in image repository {repo_name}" else: query = f"show images in image repository {repo_name}" return self.execute_query(query) diff --git a/tests/object/test_object.py b/tests/object/test_object.py index 61a6fb84d5..fdb7400d05 100644 --- a/tests/object/test_object.py +++ b/tests/object/test_object.py @@ -342,6 +342,23 @@ def test_show_with_all_options_combined(mock_execute_query, mock_cursor): mock_execute_query.assert_called_once_with(expected_query) +@mock.patch("snowflake.cli._plugins.object.manager.ObjectManager.execute_query") +def test_show_like_escapes_single_quotes(mock_execute_query, mock_cursor): + """A single quote in --like must be escaped so it cannot terminate the + LIKE literal and inject additional SQL statements.""" + from snowflake.cli._plugins.object.manager import ObjectManager + + mock_execute_query.return_value = mock_cursor(["row"], []) + + ObjectManager().show( + object_type="table", + like="foo'; drop table users; --", + ) + + expected_query = "show tables like 'foo''; drop table users; --'" + mock_execute_query.assert_called_once_with(expected_query) + + @mock.patch("snowflake.connector") @pytest.mark.parametrize( "object_type, object_name", diff --git a/tests/spcs/test_image_repository.py b/tests/spcs/test_image_repository.py index 4f29be0c74..aa8c305a2d 100644 --- a/tests/spcs/test_image_repository.py +++ b/tests/spcs/test_image_repository.py @@ -367,6 +367,22 @@ def test_list_images_with_like(mock_execute_query): assert result == cursor +@patch( + "snowflake.cli._plugins.spcs.image_repository.commands.ImageRepositoryManager.execute_query" +) +def test_list_images_like_escapes_single_quotes(mock_execute_query): + repo_name = "test_repo" + like = "foo'; drop table users; --" + cursor = Mock(spec=SnowflakeCursor) + mock_execute_query.return_value = cursor + ImageRepositoryManager().list_images(repo_name, like) + expected_query = ( + "show images like 'foo''; drop table users; --' " + "in image repository test_repo" + ) + mock_execute_query.assert_called_once_with(expected_query) + + @mock.patch("snowflake.cli._plugins.spcs.image_repository.commands.requests.get") @mock.patch(EXECUTE_QUERY) @mock.patch(