Skip to content
Open
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
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/snowflake/cli/_plugins/object/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
17 changes: 17 additions & 0 deletions tests/object/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
16 changes: 16 additions & 0 deletions tests/spcs/test_image_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading