no-dynamic-rpc-name
Call `.rpc()` with a literal function name
Call
.rpc()with a literal function name.
- CWE: CWE-913 — Improper Control of Dynamically-Managed Code Resources
- OWASP: A03:2021 — Injection
- CVSS: 8.1 (High)
- Recommended:
error
Why
supabase.rpc(name, args) executes a Postgres function by name over PostgREST. When the name is computed, the caller chooses which function runs — and a Supabase project's exposed schema routinely holds privileged helpers next to the intended one: delete_user, grant_admin, reset_password.
This is the same shape as a dynamic require(): the danger is not the arguments, it is that the callee is data.
Rule details
Fires only in files importing a @supabase/* package. Reports .rpc(x) where x is not one of a set of names this file fixes.
Fixed, and not reported — followed within the file, bounded:
- a string literal, and a template literal whose every interpolation is itself fixed or a number / boolean literal (
`get_profile`,`stats_v${V}`withconst V = 2); - a
constholding one, and a ternary between fixed names; - a lookup in a same-file object of literal names (
RPC[kind],RPC.single), including anas consttable; - a parameter whose declared type is a union of string literals (
name: 'get_a' | 'get_b'), directly or through a same-filetypealias — whatever a caller passes, the compiler holds it to that list.
A string parameter, a request field, a reassigned let, a table with a computed or spread entry, and anything imported are not fixed, and report. A spread argument is unreadable from the call site and is left alone.
Incorrect
import { createClient } from '@supabase/supabase-js';
await db.rpc(req.body.fn, args);
await db.rpc(`get_${kind}`, args);Correct
import { createClient } from '@supabase/supabase-js';
await db.rpc('get_user_profile', { id });
// Branch to a literal call rather than computing the name.
if (kind === 'profile') return db.rpc('get_user_profile', { id });
return db.rpc('get_user_orders', { id });Choosing between functions safely
Pick from a closed set the file can see, and the rule stays silent:
const FN = { profile: 'get_user_profile', orders: 'get_user_orders' } as const;
export async function load(kind: keyof typeof FN, id: string) {
return db.rpc(FN[kind], { id });
}A generic wrapper that takes the name as a string is still reported: nothing at the call constrains what a caller passes. Narrow the parameter's type to a union of the function names, or move the choice into the wrapper.
Further reading
Use this rule
npm install -D oxlint-plugin-supabase-security{ "jsPlugins": ["oxlint-plugin-supabase-security"], "rules": { "supabase-security/no-dynamic-rpc-name": "error" }}import { defineConfig } from 'vite-plus';export default defineConfig({ lint: { jsPlugins: ['oxlint-plugin-supabase-security'], rules: { 'supabase-security/no-dynamic-rpc-name': 'error', }, },});oxlint-plugin-supabase-security re-exports eslint-plugin-supabase-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.