Skip to content

Fix SQL injection, TLS, and stored XSS findings - #31797

Queued
harshach wants to merge 20 commits into
open-metadata:mainfrom
harshach:harshach/fix-security-review-findings
Queued

Fix SQL injection, TLS, and stored XSS findings#31797
harshach wants to merge 20 commits into
open-metadata:mainfrom
harshach:harshach/fix-security-review-findings

Conversation

@harshach

@harshach harshach commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Addresses the 18 OpenMetadata findings from security run 32324515413 by binding SQL values and dialect-quoting identifiers across the affected database connectors.

Removes Elasticsearch's unverified TLS fallback and renders feed mentions and advanced-search labels as safe React text.

Adds focused regression coverage for the connector query paths, TLS configuration, and both stored-XSS sinks.

Validation: 176 focused Python tests plus four subtests and 65 Jest tests passed; Ruff and targeted ESLint also passed.

Greptile Summary

The PR hardens database connector queries by binding values and dialect-quoting identifiers, removes Elasticsearch’s unverified TLS fallback, and replaces unsafe UI HTML rendering with React-safe text.

  • Updates query construction across Databricks, Doris, Hive, Oracle, QuestDB, Redshift, and Snowflake.
  • Enforces certificate verification for Elasticsearch connections.
  • Secures activity-feed mentions and advanced-search labels against stored XSS.
  • Adds focused connector, TLS, and UI regression tests.

Confidence Score: 4/5

The PR is not yet safe to merge because Snowflake GET_DDL reflection still passes object names as bind parameters and can omit ingested definitions.

Snowflake’s table, view, stream, and semantic-view definition paths still bind the object-name argument despite the connector’s documented string-literal requirement, so those reflection calls can fail and leave schema definitions missing.

Files Needing Attention: ingestion/src/metadata/ingestion/source/database/snowflake/queries.py, ingestion/src/metadata/ingestion/source/database/snowflake/utils.py

Important Files Changed

Filename Overview
ingestion/src/metadata/ingestion/source/database/snowflake/queries.py Converts Snowflake query values to bindings and identifiers to quoted substitutions across metadata queries.
ingestion/src/metadata/ingestion/source/database/snowflake/utils.py Updates Snowflake reflection helpers to pass bound values and safely constructed qualified identifiers.
ingestion/src/metadata/ingestion/source/search/elasticsearch/connection.py Removes the unverified TLS fallback so certificate-validation failures remain fatal.
openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx Renders mention content through React-safe text instead of an HTML injection sink.
openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx Changes advanced-search label highlighting to escaped React output.

Reviews (8): Last reviewed commit: "Merge branch 'main' into harshach/fix-se..." | Re-trigger Greptile

Context used:

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Aug 20, 2026
@harshach
harshach marked this pull request as ready for review August 20, 2026 04:56
Copilot AI lite review requested due to automatic review settings August 20, 2026 04:56
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

Comment thread ingestion/src/metadata/ingestion/source/database/snowflake/queries.py Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses multiple security findings across the ingestion connectors and UI by (1) binding SQL parameters / dialect-quoting identifiers to reduce SQL injection risk, (2) removing Elasticsearch’s “unverified TLS” fallback, and (3) removing stored-XSS sinks in advanced search dropdown labels and feed mention rendering.

Changes:

  • UI: Replace dangerouslySetInnerHTML-based highlighting with React text/<mark> rendering for advanced search dropdowns, and render feed mention suggestions as React text instead of interpolated HTML strings.
  • Ingestion: Bind SQL values and quote identifiers across Redshift, Snowflake, Databricks, Doris, QuestDB, Hive, and Oracle; remove Elasticsearch unverified TLS context fallback.
  • Tests: Add focused unit coverage to assert binding/quoting behavior, TLS enforcement, and XSS payloads render as text.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.tsx Removes dangerouslySetInnerHTML and renders highlighted matches as React nodes.
openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx Adds regression test ensuring dropdown labels render payloads as text (no executable HTML).
openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchPureUtils.ts Escapes HTML + regex metacharacters when generating <mark>-based highlight HTML strings.
openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx Renders mention suggestion items via React into a DOM container instead of string-built HTML.
openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.test.tsx Adds regression coverage ensuring mention names/breadcrumbs render as text (no XSS).
ingestion/tests/unit/topology/database/test_redshift_utils.py Adds unit test asserting schema filter is bound (not interpolated) for Redshift schema-column query.
ingestion/tests/unit/topology/database/test_redshift_incremental_table_processor.py Adds unit test asserting database/start_date are passed as bound params.
ingestion/tests/unit/topology/database/test_questdb.py Adds unit test asserting table name is bound in QuestDB table_columns query.
ingestion/tests/unit/topology/database/test_oracle.py Updates tests for bound schema param in stored procedure queries; adds injection regression.
ingestion/tests/unit/topology/database/test_hive.py Updates tests to pass a quoting dialect; adds injection regression for catalog/table quoting.
ingestion/tests/unit/topology/database/test_doris.py Adds unit test validating identifier quoting escapes embedded backticks.
ingestion/tests/unit/topology/database/test_databricks_query_reduction.py Adds unit test verifying identifier helper quoting keeps injected text inside backticks.
ingestion/tests/unit/source/search/elasticsearch/test_connection.py Adds test asserting empty cert config does not disable TLS verification (raises).
ingestion/tests/unit/source/database/snowflake/test_queries.py Adds tests for identifier quoting and schema binding in Snowflake table-name queries.
ingestion/src/metadata/ingestion/source/search/elasticsearch/connection.py Removes unverified TLS context fallback; errors on incomplete SSL cert configuration.
ingestion/src/metadata/ingestion/source/database/snowflake/utils.py Adds identifier quoting helpers; switches multiple queries to use bound params / safer formatting.
ingestion/src/metadata/ingestion/source/database/snowflake/queries.py Converts interpolated Snowflake query strings to bind parameters and quoted identifier slots.
ingestion/src/metadata/ingestion/source/database/snowflake/metadata.py Quotes database/schema identifiers for SHOW ... IN ... queries; binds stream name.
ingestion/src/metadata/ingestion/source/database/redshift/utils.py Binds schema param for schema-column query instead of interpolating.
ingestion/src/metadata/ingestion/source/database/redshift/queries.py Replaces interpolated schema/database/start_date with bound parameters.
ingestion/src/metadata/ingestion/source/database/redshift/metadata.py Binds database name for external table location query.
ingestion/src/metadata/ingestion/source/database/redshift/incremental_table_processor.py Binds database/start_date parameters when querying table changes.
ingestion/src/metadata/ingestion/source/database/questdb/utils.py Executes QuestDB columns query with a bound parameter.
ingestion/src/metadata/ingestion/source/database/questdb/queries.py Updates QuestDB query to use :table_name bind parameter.
ingestion/src/metadata/ingestion/source/database/oracle/queries.py Replaces interpolated schema with :schema bind parameter.
ingestion/src/metadata/ingestion/source/database/oracle/metadata.py Executes stored procedure query using bound schema parameter.
ingestion/src/metadata/ingestion/source/database/hive/utils.py Quotes identifiers for DESCRIBE/SHOW CREATE paths to avoid injection.
ingestion/src/metadata/ingestion/source/database/hive/queries.py Adjusts DESCRIBE template to accept a pre-qualified (quoted) table name.
ingestion/src/metadata/ingestion/source/database/doris/queries.py Adjusts SHOW templates to accept a pre-qualified (quoted) table name.
ingestion/src/metadata/ingestion/source/database/doris/metadata.py Adds qualified-identifier helper; uses it for SHOW FULL COLUMNS / SHOW PARTITIONS queries.
ingestion/src/metadata/ingestion/source/database/databricks/queries.py Removes raw backtick interpolation points; uses identifier placeholders.
ingestion/src/metadata/ingestion/source/database/databricks/metadata.py Adds identifier quoting helpers and formats identifier substitutions via the dialect.
Suppressed comments (1)

ingestion/src/metadata/ingestion/source/database/snowflake/utils.py:347

  • Same as get_table_names: parameters contains formatting-only keys that are not bind params in the formatted SQL. Passing them to execute() may fail with “text() construct doesn't define a bound parameter …”. Filter to only the bind params actually used by the query.
    cursor = connection.execute(text(query.format(**parameters)), parameters)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ingestion/src/metadata/ingestion/source/database/snowflake/utils.py Outdated
Copilot AI review requested due to automatic review settings August 20, 2026 05:02
@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs and removed safe to test Add this label to run secure Github workflows on PRs labels Aug 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

ingestion/src/metadata/ingestion/source/database/snowflake/utils.py:348

  • Same issue as get_table_names: _get_query_parameters returns keys used for string formatting (include_transient_tables, include_views, account_usage) in addition to bind values. Passing the full dict to execute() can fail due to extra, undefined bind parameters.
        query = queries["views"]

    cursor = connection.execute(text(query.format(**parameters)), parameters)
    result = SnowflakeTableList(

Comment thread ingestion/src/metadata/ingestion/source/database/snowflake/utils.py
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 70%
70.44% (95220/135166) 54.98% (56342/102461) 56.35% (18807/33371)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It spans security-sensitive changes across multiple connectors, TLS behavior, and UI sinks, which warrants final human review despite the added regression coverage.

Review details
  • Files reviewed: 46/46 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The changes span multiple connectors plus UI rendering/security paths, so a final human review is needed to validate runtime behavior across environments beyond the available unit/E2E regressions.

Review details
  • Files reviewed: 46/46 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

chirag-madlani
chirag-madlani previously approved these changes Sep 3, 2026
mohityadav766
mohityadav766 previously approved these changes Sep 3, 2026
ulixius9
ulixius9 previously approved these changes Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — manual (2026-09-03T17:03:31Z)

Blocked the queue: ui-checkstyle

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

🚦 Removed from the merge queue — failed_checks (2026-09-04T20:07:55Z)

Blocked the queue: playwright-summary

harshach and others added 2 commits September 6, 2026 01:06
Conflict resolutions:
- snowflake/queries.py, query_parser.py, tests/.../test_queries.py: main moved
  QUERY_TAG to a driver session parameter at connection creation (open-metadata#32481), which
  removes the SQL-string path the branch was hardening. Dropped
  set_session_tag_query and its test; kept _snowflake_string_literal, which
  build_get_ddl_query still uses.
- playwright BigQueryIngestionClass.ts: main landed the same select-flake fix as
  a shared selectOptionWithRetry util, so took main's version and dropped the
  branch-local selectReactAriaOption duplicate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clean auto-merge: main's 8 new commits touch no file this branch changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Snowflake tag-ingestion queries in snowflake/metadata.py still format database_name/schema_name into SQL templates that use string-literal interpolation, leaving confirmed SQL-injection sinks in modified code paths.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (2)

ingestion/src/metadata/ingestion/source/database/snowflake/metadata.py:391

  • database_name is still formatted into the Snowflake database-tag query template (which uses where OBJECT_DATABASE = '{database_name}'), so a malicious database name containing quotes can break out of the string literal. Please switch these tag queries to bind :database_name (and any other value filters) and pass the parameters to execute(), leaving only {account_usage} as a formatted/quoted identifier.
                for row in conn.execute(
                    text(
                        SNOWFLAKE_FETCH_DATABASE_TAGS.format(
                            database_name=database_name,
                            account_usage=quote_account_usage_schema(self.service_connection.accountUsageSchema),
                        )
                    )

ingestion/src/metadata/ingestion/source/database/snowflake/metadata.py:633

  • The fallback path that wraps names with f'"{...}"' is not safe quoting (embedded " will break the identifier) and it still relies on string formatting into the SQL template. Prefer using the Snowflake identifier-quoting helpers for identifier parts and bind parameters for value filters, rather than constructing quoted SQL fragments with f-strings.
                    logger.warning(f"Error fetching tags {exc}. Trying with quoted names")
                    result = self.connection.execute(
                        text(
                            SNOWFLAKE_FETCH_TABLE_TAGS.format(
                                database_name=f'"{self.context.get().database}"',
                                schema_name=f'"{self.context.get().database_schema}"',
                                account_usage=quote_account_usage_schema(self.service_connection.accountUsageSchema),
                            )
                        )
                    )
  • Files reviewed: 45/45 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread ingestion/src/metadata/ingestion/source/database/snowflake/metadata.py Outdated
Comment thread ingestion/src/metadata/ingestion/source/database/snowflake/metadata.py Outdated
The three ACCOUNT_USAGE.tag_references templates interpolated
database_name/schema_name into `where OBJECT_DATABASE = '{...}'` string
literals. Snowflake permits quotes in quoted identifiers, so a database
created as "PROD' OR 1=1 UNION SELECT ..." escapes the literal and runs
attacker SQL under the ingestion role.

Bind both object names instead, leaving only {account_usage} as a
formatted (and already quoted) identifier.

The yield_tag retry path wrapped the context names in f'"{...}"'. That was
never valid quoting for a value comparison -- tag_references stores plain
names, so '"MYDB"' matches nothing -- and embedded quotes broke it anyway.
It now binds fqn.unquote_name(...) of the context names, which makes the
retry meaningful for names that arrive already quoted.

Reported by Copilot on open-metadata#31797.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

One Oracle ingestion module uses runtime-evaluated subscripted type annotations without postponed evaluation, which should be hardened to avoid import-time typing issues across SQLAlchemy versions.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

ingestion/src/metadata/ingestion/source/database/oracle/metadata.py:18

  • process_result uses a parameterized Row[Any] type annotation, but this module does not enable postponed evaluation of annotations. Adding from __future__ import annotations (as used in other ingestion database modules) avoids runtime evaluation of subscripted typing and keeps import-time behavior robust across SQLAlchemy versions.
  • Files reviewed: 45/45 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@github-actions

github-actions Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It makes broad, security-sensitive changes across multiple connectors and UI sinks that warrant final human verification of runtime behavior beyond the included regression tests.

Review details
  • Files reviewed: 45/45 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

harshach and others added 2 commits September 7, 2026 08:40
Two CI gates broke on the previous commit, both from moving those
expressions rather than from new behaviour:

- basedpyright: the baseline matches entries by column range, so
  reindenting `self.context.get().database` into the bind dict orphaned
  its baseline entries and the errors resurfaced. Tag the three lines with
  `# pyright: ignore[reportAttributeAccessIssue]`, as the ~15 other
  TopologyContext reads in this file already do.

- check_ruff_suppressions: the G004 baseline keys on a hash of the log
  message, so rewording the retry warning registered as a new f-string
  log. Switch it to lazy %-style logging, which drops the violation
  outright, and prune the now-obsolete baseline entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ngs' into harshach/fix-security-review-findings

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It touches multiple security-critical paths (SQL construction, TLS verification, and XSS sinks) across many connectors/modules and should receive final human verification despite the added tests.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchPureUtils.ts:14

  • This file now imports OldJsonTree from @react-awesome-query-builder/antd, but the rest of the UI codebase (including QueryBuilder helpers and the advance-search provider) consistently imports these types from @react-awesome-query-builder/ui. Keeping this consistent avoids type drift/confusion across query-builder utilities.
    openmetadata-ui/src/main/resources/ui/src/utils/AdvancedSearchUtils.test.tsx:15
  • FieldOrGroup is imported from @react-awesome-query-builder/antd here, but the production code under test (AdvancedSearchUtils.tsx) imports it from @react-awesome-query-builder/ui. Align the import to the same entrypoint to avoid accidental type/API mismatches between test and implementation.
  • Files reviewed: 46/46 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It is a large, security-sensitive, cross-connector change spanning multiple SQL dialects and UI rendering paths, so it warrants final human review despite strong regression coverage.

Review details
  • Files reviewed: 46/46 changed files
  • Comments generated: 1
  • Review effort level: Lite

*/

import { FieldOrGroup } from '@react-awesome-query-builder/ui';
import { FieldOrGroup } from '@react-awesome-query-builder/antd';
@gitar-bot

gitar-bot Bot commented Sep 8, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Hardens database connector queries by binding SQL values and dialect-quoting identifiers across Databricks, Doris, Hive, Oracle, QuestDB, Redshift, and Snowflake connectors. Removes Elasticsearch's unverified TLS fallback and secures activity-feed mentions and advanced-search labels against stored XSS by rendering through React-safe text instead of HTML injection sinks. Addresses ReactDOM.render memory leak on mention renders and fixes unescaped account_usage identifier interpolation. Includes 176 focused Python tests plus four subtests and 65 Jest tests; Ruff and targeted ESLint also passed.

✅ 2 resolved
Performance: ReactDOM.render into detached node leaks on every mention render

📄 openmetadata-ui/src/main/resources/ui/src/components/ActivityFeed/FeedEditor/FeedEditor.tsx:173-187
In FeedEditor.tsx renderItems now calls ReactDOM.render(<jsx/>, wrapper) on a freshly created detached <div> and returns it to react-quill-new's mention module. React 18.2 renders this synchronously (so the feature works), but the component is never unmounted — there is no ReactDOM.unmountComponentAtNode call. Since renderItem runs for every suggestion each time the mention dropdown updates, each keystroke leaks a React fiber tree and its listeners for the lifetime of the editor. Consider rendering the suggestion markup without a persistent React root (e.g. render to a string/element once) or tracking the wrapper and unmounting it when the mention list closes.

Security: account_usage identifier still interpolated into SQL unescaped

📄 ingestion/src/metadata/ingestion/source/database/snowflake/utils.py:277 📄 ingestion/src/metadata/ingestion/source/database/snowflake/metadata.py:373
In _get_query_parameters (utils.py:277) the account_usage value is placed in format_parameters and .format()-ed directly into the query text (e.g. from {account_usage}.tables) with no quoting or escaping, unlike schema/database/date which this PR converted to bind parameters. account_usage originates from user-controlled service-connection config (self.service_connection.accountUsageSchema, metadata.py:373), so it remains an injection sink that the surrounding hardening did not close. Since it is a compound identifier (e.g. SNOWFLAKE.ACCOUNT_USAGE) it cannot be a bind param, but it should be validated or split-and-dialect-quoted via the existing _quote_identifier/_qualified_identifier helpers before interpolation. Note this is a pre-existing pattern used across many queries in queries.py, not newly introduced here.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The change set is security-critical and spans many connectors and UI rendering paths, so it warrants final human review to confirm no behavioral regressions across all affected integrations.

Review details
  • Files reviewed: 46/46 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

@sonarqubecloud

sonarqubecloud Bot commented Sep 8, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend safe to test Add this label to run secure Github workflows on PRs skip-pr-checks Bypass PR metadata validation check To release Will cherry-pick this PR into the release branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants