con un clic
safe-sql-execution
// Safely execute SQL queries against a user database without risking SQL injection or other security vulnerabilities.
// Safely execute SQL queries against a user database without risking SQL injection or other security vulnerabilities.
Design system UI patterns for Supabase Studio. Use when building or updating pages, forms, tables, charts, empty states, navigation, cards, alerts, or side panels (sheets). Covers layout selection, component choice, and placement conventions.
Use when reviewing PRs that touch packages/dev-tools/, packages/common/posthog-client.ts, or packages/common/feature-flags.tsx. Covers environment guards, flag override cookies, telemetry event subscription, and SSE stream safety.
React and TypeScript best practices for Supabase Studio. Use when writing or reviewing Studio components โ covers boolean naming, component structure, loading/error states, state management, custom hooks, event handlers, conditional rendering, performance, and TypeScript conventions.
Write and run Playwright E2E tests for Supabase Studio. Use when asked to run e2e tests, write new E2E tests, or debug flaky tests. Covers running commands, avoiding race conditions, waiting strategies, selectors, helper functions, and CI vs local differences.
React Query conventions for data fetching in Supabase Studio. Use when writing or reviewing query hooks, mutation hooks, or query keys in apps/studio/data/. Covers queryOptions pattern, keys.ts structure, mutation hook template, and imperative fetching.
Testing strategy for Supabase Studio. Use when writing tests, deciding what type of test to write, extracting logic from components into testable utility functions, or reviewing test coverage. Covers unit tests, component tests, and E2E test selection criteria.
| name | safe-sql-execution |
| description | Safely execute SQL queries against a user database without risking SQL injection or other security vulnerabilities. |
Supabase Studio executes SQL statements directly against the user's database. Because this is the authenticated user's own database, our security model is different from most frontend applications: a user should be able to execute any SQL statement, as long as it is proven that they themselves authored it. What we SHOULD NOT ALLOW is execution of SQL statements that can be influenced by an attacker, such as through URL parameters.
The security model for SQL execution in Supabase Studio is based on the principle of "proven authorship". This means that a user should only be able to execute SQL statements that they have explicitly authored, and not statements that can be influenced by external input.
There are three classes of SQL fragments:
Hardcoded within the application code. These are safe to execute because
they cannot be influenced by an attacker. They can be marked with the
safeSql utility with pg-meta:
import { safeSql } from '@supabase/pg-meta'
const sql = safeSql`
SELECT *
FROM users
WHERE id = 1
`
safeSql automatically creates a string of the branded type
SafeSqlFragment. (See Provenance Tracking below.)
Third-party influenceable. These are SQL fragments that can be influenced
by an attacker, such as through URL parameters or LLM output. These should
be marked with the untrustedSql utility with pg-meta:
import { untrustedSql } from '@supabase/pg-meta'
const unsafeQuery = searchParams.get('query')
const querySql = untrustedSql(unsafeQuery)
untrustedSql creates a string of the branded type UntrustedSqlFragment.
(See Provenance Tracking below.)
User-authored. These are SQL fragments that are authored by the user themselves within the UI, for example in a text input field. Because the user is the author, these should be considered safe to execute.
However, there is a caveat, where third-party and user-authored code can mix, contaminating the user-authored code (for example, if an input is prefilled from an unsanitized URL parameter). Provenance tracking helps us track these cases.
For example, a safe input component could be implemented as follows by requiring that its placeholder and controlled value are of type SafeSqlFragment. In this case we can use its onChange to promote the user input to SafeSqlFragment type, because we know that the user is the author of the input. An implementation of this is in
@apps/studio/components/ui/SafeSqlInput.tsx:
import { rawSql, type SafeSqlFragment } from '@supabase/pg-meta'
import type { ChangeEvent, ComponentProps } from 'react'
import { Input } from 'ui-patterns/DataInputs/Input'
type InputProps = ComponentProps<typeof Input>
export type SafeSqlInputProps = Omit<
InputProps, 'placeholder' | 'value' | 'onChange'
> & {
placeholder?: SafeSqlFragment
value: SafeSqlFragment
onChange?:
(event: ChangeEvent<HTMLInputElement>, value: SafeSqlFragment) => void
}
export const SafeSqlInput = ({ onChange, ...props }: SafeSqlInputProps) => (
<Input
{...props}
onChange={(event) => onChange?.(event, rawSql(event.target.value))}
/>
)
This is pretty much the ONLY VALID USE CASE of the rawSql export from pg-meta, and it should be used with caution.
Branded types are used to track the provenance of SQL fragments. The types,
exported from pg-meta, are:
SafeSqlFragment: represents SQL fragments that are safe to execute, because
they are either hardcoded in the application or authored by the user
themselves.UntrustedSqlFragment: represents SQL fragments that can be influenced by an
attacker, such as through URL parameters or LLM output.These are valid ways to generate a SafeSqlFragment:
safeSql utility from pg-meta to create hardcoded SQL fragments.pg-meta to sanitize untrusted input
and promote it to a SafeSqlFragment:
identliteralkeywordjoinSqlFragmentstrimSafeSqlFragmentUntrustedSqlFragments can be generated from raw strings using
untrustedSql().
There is also a union type, DisplayableSqlFragment, which represents SQL fragments that can be safely displayed in the UI, but not necessarily executed. This includes both SafeSqlFragment and UntrustedSqlFragment.
SQL derived directly from catalog tables (e.g., function definitions, RLS expressions, etc.) is considered safe, and it is promoted AT THE POINT OF BEING QUERIED from the database. In most cases, this is in an apps/studio/data/*/.ts file, in the utility function that makes the API or database fetch.
A critical exception to the safety of SQL round-tripped from the database is
user snippets. These must NEVER BE CONSIDERED SAFE because they are both (a)
externally influenceable and (b) auto-saved. The snippet type uses the
unchecked_sql property, which is an UntrustedSqlFragment, to enforce this.
SafeSqlFragment typeGiven an insecure string or UntrustedSqlFragment, how do we promote it safely
to a SafeSqlFragment?
This is the preferred method when the input is sanitizable, e.g., it is a relation name, a column name, will be compared as a literal, etc.
The pg-meta library provides the following sanitization utilities that can be
used to safely promote untrusted input to SafeSqlFragment:
ident: for sanitizing identifiers such as table names or column names.literal: for sanitizing literal values that will be used in SQL statements.keyword: for sanitizing SQL keywords.acceptUntrustedSqlSome untrusted SQL fragments cannot be sanitized with the above utilities. For
example, the USING expression in the RLS policy editor is an arbitrary SQL
expression.
In these cases, we can promote the SQL fragment upon explicit user action. User action indicates that the user has seen the SQL and is OK with running it. For example, an explicit user action could be clicking a "Run" button.
The promotion happens with the acceptUntrustedSql utility from pg-meta,
which takes an UntrustedSqlFragment and returns a SafeSqlFragment.
This utility MUST ONLY BE USED IN event handlers. It should NEVER be used in a useQuery, direct in the render body of a component, in a useEffect, or anywhere it could auto-run without explicit user action.
This is safe:
import { acceptUntrustedSql } from '@supabase/pg-meta'
function SafeComponent() {
const { mutate: execute } = useExecuteSqlMutation()
const handleRun = () => {
// โ
GOOD: Safe because it is in an event handler which requires a user
// click
execute({ sql: acceptUntrustedSql(/* sql */) })
}
return (
<button onClick={handleRun}>Run</button>
)
}
This is unsafe:
import { acceptUntrustedSql } from '@supabase/pg-meta'
function UnsafeComponent() {
const { data } = useQuery({
queryKey: ['execute-sql', sql],
queryFn: () => {
// ๐ BAD: Unsafe because it is in a query which could auto-run without
// explicit user action
return execute({ sql: acceptUntrustedSql(/* sql */) })
},
})
}
SQL run against the user's Postgres database runs through the executeSql
function, which only takes arguments of type SafeSqlFragment for the SQL
parameter. Raw strings or UntrustedSqlFragments will error at compile time.
// โ
GOOD: Automatically safe with `safeSql` utility
const selectStatement = safeSql`select 1`
// โ
GOOD: `pg-meta` utilities sanitize the input
const tableName = ident(userInputTableName)
const searchString = literal(userInputSearchString)
const sqlStatement = safeSql`
SELECT *
FROM ${tableName}
WHERE search_column = ${searchString}
`
// ๐ BAD: Passing raw strings will type error
const tableName = 'my_table'
const sqlStatement = safeSql`
SELECT *
FROM ${tableName}
`
// โ
GOOD: SafeSqlInput only allows a value that is a SafeSqlFragment
import { SafeSqlInput } from '@apps/studio/components/ui/SafeSqlInput'
function MyComponent() {
const [sql, setSql] = useState<SafeSqlFragment>(safeSql``)
return (
<SafeSqlInput
placeholder={safeSql`Enter your SQL query here...`}
value={sql}
onChange={(event, value) => setSql(value)}
/>
)
}
// ๐ BAD: This input mixes SafeSqlFragments and unsafe strings
function MyBadComponent() {
const [sql, setSql] = useState<SafeSqlFragment>(safeSql``)
return (
<Input
// ๐ BAD: This is unsafe because the placeholder is a raw string
placeholder="Enter your SQL query here..."
value={sql}
onChange={(event) => setSql(event.target.value)}
/>
)
}
// โ
GOOD: SQL from the database is promoted to SafeSqlFragment at the point
// of fetching
// data/function-definitions.ts
function markFunctionDefinitionSafe(
functionDefinition: FunctionDefinition
): SafeFunctionDefinition {
return {
...functionDefinition,
definition: functionDefinition.definition as SafeSqlFragment,
}
}
// data/function-definitions.ts
function getFunctionDefinitions() {
return GET(`/function-definitions`).then((functionDefinitions) =>
functionDefinitions.map(markFunctionDefinitionSafe)
)
}
// ๐ BAD: Strings are promoted to SafeSqlFragment in a utility function, where
// it is impossible to easily determine the safety of the input
// utils.ts
function markFunctionDefinitionSafe(
functionDefinition: FunctionDefinition
): SafeFunctionDefinition {
return {
...functionDefinition,
definition: functionDefinition.definition as SafeSqlFragment,
}
}
// Component.ts
function MyComponent() {
const { data: functionDefinitions } = useFunctionDefinitions()
const safeFunctionDefinitions = functionDefinitions.map(markFunctionDefinitionSafe)
}
Snippets are auto-persisted to the database and can be created or modified
through externally influenceable channels (e.g., prefilled from URL params).
The unchecked_sql property is typed as UntrustedSqlFragment to enforce this
โ it must only be promoted to SafeSqlFragment via acceptUntrustedSql in an
event handler that requires explicit user action.
// ๐ BAD: Snippet content is executed automatically via useQuery, with no
// explicit user action confirming that the user has reviewed the SQL.
import { acceptUntrustedSql } from '@supabase/pg-meta'
function UnsafeSnippetPreview({ snippet }: { snippet: Snippet }) {
const { data } = useExecuteSqlQuery({
sql: acceptUntrustedSql(snippet.content.unchecked_sql),
})
return <Results data={data} />
}
// ๐ BAD: Casting bypasses the type system entirely. The snippet's
// `unchecked_sql` is `UntrustedSqlFragment` for a reason โ never cast it.
function UnsafeSnippetRunner({ snippet }: { snippet: Snippet }) {
const { mutate: execute } = useExecuteSqlMutation()
useEffect(() => {
execute({ sql: snippet.content.unchecked_sql as SafeSqlFragment })
}, [snippet])
}
// โ
GOOD: Snippet content is only promoted to SafeSqlFragment inside an event
// handler, after the user clicks Run. The user has seen the SQL in the editor
// and explicitly chosen to execute it.
import { acceptUntrustedSql } from '@supabase/pg-meta'
function SnippetRunner({ snippet }: { snippet: Snippet }) {
const { mutate: execute } = useExecuteSqlMutation()
const handleRun = () => {
execute({ sql: acceptUntrustedSql(snippet.content.unchecked_sql) })
}
return (
<>
<SnippetEditor snippet={snippet} />
<button onClick={handleRun}>Run</button>
</>
)
}