Skip to content

sensitiveColumnsExposed

Group security · Severity Error · Recommended · Requires Supabase
Diagnostic splinter/security/sensitiveColumnsExposed

Note

This rule needs a Supabase database. It is skipped when the Supabase roles don't exist.

Description

Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection.

Remediation

See the Supabase database linter docs.

How to configure

{
  "splinter": {
    "rules": {
      "security": {
        "sensitiveColumnsExposed": "error"
      }
    }
  }
}

SQL query

Show the query
(
with sensitive_patterns as (
    select unnest(array[
        -- Authentication & Credentials
        'password', 'passwd', 'pwd', 'passphrase',
        'secret', 'secret_key', 'private_key', 'api_key', 'apikey',
        'auth_key', 'token', 'jwt', 'access_token', 'refresh_token',
        'oauth_token', 'session_token', 'bearer_token', 'auth_code',
        'session_id', 'session_key', 'session_secret',
        'recovery_code', 'backup_code', 'verification_code',
        'otp', 'two_factor', '2fa_secret', '2fa_code',
        -- Personal Identifiers
        'ssn', 'social_security', 'social_security_number',
        'driver_license', 'drivers_license', 'license_number',
        'passport_number', 'passport_id', 'national_id', 'tax_id',
        -- Financial Information
        'credit_card', 'card_number', 'cvv', 'cvc', 'cvn',
        'bank_account', 'account_number', 'routing_number',
        'iban', 'swift_code', 'bic',
        -- Health & Medical
        'health_record', 'medical_record', 'patient_id',
        'insurance_number', 'health_insurance', 'medical_insurance',
        'treatment',
        -- Device Identifiers
        'mac_address', 'macaddr', 'imei', 'device_uuid',
        -- Digital Keys & Certificates
        'pgp_key', 'gpg_key', 'ssh_key', 'certificate',
        'license_key', 'activation_key',
        -- Biometric Data
        'facial_recognition'
    ]) as pattern
),
exposed_tables as (
    select
        n.nspname as schema_name,
        c.relname as table_name,
        c.oid as table_oid
    from
        pg_catalog.pg_class c
        join pg_catalog.pg_namespace n
            on c.relnamespace = n.oid
    where
        c.relkind = 'r' -- regular tables
        and (
            pg_catalog.has_table_privilege('anon', c.oid, 'SELECT')
            or pg_catalog.has_table_privilege('authenticated', c.oid, 'SELECT')
        )
        and n.nspname = any(array(select trim(unnest(string_to_array(current_setting('pgrst.db_schemas', 't'), ',')))))
        and n.nspname not in (
            '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_config', '_timescaledb_internal', 'auth', 'cron', 'extensions', 'graphql', 'graphql_public', 'information_schema', 'net', 'pgmq', 'pgroonga', 'pgsodium', 'pgsodium_masks', 'pgtle', 'pgbouncer', 'pg_catalog', 'pgtle', 'realtime', 'repack', 'storage', 'supabase_functions', 'supabase_migrations', 'tiger', 'topology', 'vault'
        )
        -- Only flag tables without RLS enabled
        and not c.relrowsecurity
),
sensitive_columns as (
    select
        et.schema_name,
        et.table_name,
        a.attname as column_name,
        sp.pattern as matched_pattern
    from
        exposed_tables et
        join pg_catalog.pg_attribute a
            on a.attrelid = et.table_oid
            and a.attnum > 0
            and not a.attisdropped
        cross join sensitive_patterns sp
    where
        -- Match column name against sensitive patterns (case insensitive), allowing '-'/'_' variants
        replace(lower(a.attname), '-', '_') = sp.pattern
)
select
    'sensitive_columns_exposed' as "name!",
    'Sensitive Columns Exposed' as "title!",
    'ERROR' as "level!",
    'EXTERNAL' as "facing!",
    array['SECURITY'] as "categories!",
    'Detects tables exposed via API that contain columns with potentially sensitive data (PII, credentials, financial info) without RLS protection.' as "description!",
    format(
        'Table `%s.%s` is exposed via API without RLS and contains potentially sensitive column(s): %s. This may lead to data exposure.',
        schema_name,
        table_name,
        string_agg(distinct column_name, ', ' order by column_name)
    ) as "detail!",
    'https://supabase.com/docs/guides/database/database-linter?lint=0023_sensitive_columns_exposed' as "remediation!",
    jsonb_build_object(
        'schema', schema_name,
        'name', table_name,
        'type', 'table',
        'sensitive_columns', array_agg(distinct column_name order by column_name),
        'matched_patterns', array_agg(distinct matched_pattern order by matched_pattern)
    ) as "metadata!",
    format(
        'sensitive_columns_exposed_%s_%s',
        schema_name,
        table_name
    ) as "cache_key!"
from
    sensitive_columns
group by
    schema_name,
    table_name
order by
    schema_name,
    table_name)