Skip to main content
友田 陽大
Application-layer security
Next.js
Supabase
RLS
セキュリティ
TypeScript
アーキテクチャ設計

Next.js × Supabase Application Security Complete Guide — Protecting Authorization and RLS with Vulnerability Detection and Defense in Depth

The overall picture of security for AI-mass-produced Next.js × Supabase apps. We divide it into automatable horizontal controls (CSP, rate limiting, CSRF, Zod validation), injection detected by static analysis (SQLi/SSRF/XSS), and vertical risks only design can close (authorization/IDOR, RLS, tenant isolation), and systematize how to protect with 3 detection layers and defense in depth.

Published
Reading time
22 min read
Author
友田 陽大
Share
Contents

Let me state the conclusion first. Next.js × Supabase application security becomes far clearer at once if you draw the map by dividing it into "automatable horizontal controls" and "vertical risks only design can protect." The former is security headers, CSP, rate limiting, CSRF, input validation, and secret hygiene, closeable uniformly with libraries and config. The latter is authorization (IDOR), RLS design, tenant isolation, and business logic, which only a human who knows your data model can design.

This distinction matters because the world's notion of "put in a security product and you're safe" only works for horizontal controls. Vertical risks appear as a "legitimate request" with perfectly correct authentication and format, so a WAF or headers structurally can't prevent them. This article, after redrawing the boundary between the two, systematizes — based on real code and published primary sources — what to crush with automation and what to protect with design and verification. This isn't a "don't use AI" or "Supabase is dangerous" story. Building fast with AI is itself correct. It's a story about the mechanism to harden what you built fast safely, without leaking.


1. The overall picture: the 3-layer map of horizontal controls, injection classes, and vertical risks

Before getting into the details, let me hand you the map. App-layer threats split into 3 by nature and "how far automation reaches."

LayerRepresentative threatsMain way to preventWhere automation reaches
Horizontal controlsMissing headers, CSP gaps, no rate limiting, CSRF, secret leakage, env gapsApply uniformly with libraries, config, middlewareCan automate through implementation
Injection classesSQLi, SSRF, path traversal, open redirect, DOM XSSInput validation/neutralization + safe APIsCan detect with static analysis (taint)
Vertical risksAuthorization/IDOR, RLS misconfiguration, tenant crossing, business logic, privilege escalationSecure design + verification (RLS + ownership)Through detection and warning. The fix = design is human

The upper 2 layers (horizontal controls, injection classes) aren't things a human should think about every time. The correct answer is to fix them once with config and have static analysis stand guard mechanically. The problem is the 3rd layer. Because vertical risks depend on the app-specific meaning of "who owns what," a tool can point out "suspicious" but can't say "correct."

The discipline running through this entire article is the same philosophy OWASP's Application Security Verification Standard (ASVS) shows — measure security not by "did you put it in" but by "can you verify it." Hereafter, for each layer, I state "how to close it" and "how to verify it" as a set.


2. The threat premise: the holes that AI mass-produces in Next.js × Supabase

Why is this map needed now? Regarding the security of AI-generated code, there's measurement, not speculation. In Veracode's 2025 study, which set 100+ LLMs 80 coding tasks across 4 languages, 45% of the generated code contained known security flaws, and only 55% were safe. What's more important is that even as models got significantly bigger and smarter, the security scores stayed flat (Veracode 2025 GenAI Code Security Report). Code has become "more working," but not "more secure."

The reason is simple. AI realizes "what you want to do (= what works in a demo)" instructed by the prompt via the shortest distance. "Don't show other people's data" and "don't let it hit internal resources" lie outside the happy path unless explicitly requested, and absolutely don't surface in a demo. Because as long as you're touching it with your own account, no one rewrites an ID or puts an internal-metadata address in url=.

This isn't a desk-bound worry. CVE-2025-48757, registered in 2025, is one where, due to insufficient Row-Level Security policies of an AI-generation platform (Lovable, until 2025-04-15), a remote unauthenticated attacker could read and write arbitrary DB tables of generated sites. Its classification is CWE-863 (Incorrect Authorization), and the CVSS base score is 9.3 CRITICAL. A real case where a vertical-risk flaw came to production in the most ordinary and most serious form.

"It worked" ≠ "it's safe." This is the starting point of this article. On hardening AI-generated code before shipping to production, I summarize separately in Production Hardening of AI-Generated Code. This article is the map narrowed to its "app layer."


3. Horizontal controls (the automatable layer) — fix it once with config

The first thing to crush is horizontal controls. They apply uniformly across the app, and because libraries and config can take them over, they're overwhelmingly cost-effective. The trick is to not assume "a human writes it correctly every time."

3-1. Typed env boundary — validate secrets at the boundary, prevent mixing

Environment variables are "external input." Validate them once at startup, and use them type-safely thereafter. With server-only, physically prevent secrets from mixing into the client bundle.

// lib/env.server.ts — サーバー専用。クライアントに混入したらビルド時に弾く
import "server-only";
import { z } from "zod";

export const serverEnv = z
  .object({
    SUPABASE_SERVICE_ROLE_KEY: z.string().min(1),
    RESEND_API_KEY: z.string().startsWith("re_"),
  })
  .parse(process.env); // 起動時に1度。欠けていれば即クラッシュ=fail-fast

Whether the NEXT_PUBLIC_ prefix is present is critical. Expose the service_role key with NEXT_PUBLIC_ and all data leaks. The separation of responsibilities of the anon key and the service_role key is itself a large subject, so it's detailed in Handling the anon Key and service_role Key. The nature of the keys is at the primary source Supabase: API keys.

3-2. Input validation (Zod) — narrow the type at the system boundary

Structurally validate all external input at the handler's entrance. Don't trust "the form it came in as."

const Body = z.object({
  email: z.string().email(),
  phase: z.enum(["discovery", "build", "audit"]),
});

export async function POST(req: Request) {
  const parsed = Body.safeParse(await req.json());
  if (!parsed.success) return Response.json({ error: "invalid" }, { status: 400 });
  // 以後 parsed.data は型安全
}

3-3. Security headers / CSP (nonce) — shrink the damage of XSS

CSP is the last wall of "even if XSS mixes in, don't let it execute an external script." Avoid 'unsafe-inline' and issue a nonce per request is the strict version.

// middleware.ts — リクエストごとに nonce を発行し、厳格な CSP を付与する
import { NextResponse, type NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
  const csp = [
    `default-src 'self'`,
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
    `style-src 'self'`,
    `img-src 'self' data:`,
    `object-src 'none'`,
    `base-uri 'none'`,
    `frame-ancestors 'none'`, // クリックジャッキング対策
  ].join("; ");

  const headers = new Headers(request.headers);
  headers.set("x-nonce", nonce); // Server Component から読めるよう引き回す

  const res = NextResponse.next({ request: { headers } });
  res.headers.set("Content-Security-Policy", csp);
  return res;
}

Together, attach Strict-Transport-Security, X-Content-Type-Options: nosniff, and Referrer-Policy. These are typical horizontal controls that take effect on all requests once written.

3-4. Rate limiting — in serverless, put the state in a "shared store"

This is where AI-generated code most often goes wrong. Because serverless functions are disposed of per request, an in-process Map counter doesn't work (if executed on a different instance, the count resets). Put the state in an external shared store.

// レート制限:状態はプロセス外(Redis 等)へ。メモリ内カウンタはサーバーレスで無効
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, "60 s"),
});

export async function POST(req: Request) {
  const ip = req.headers.get("x-forwarded-for") ?? "anonymous";
  const { success } = await ratelimit.limit(ip);
  if (!success) return new Response("Too Many Requests", { status: 429 });
  // 本処理…
}

3-5. CSRF / Origin verification — confirm the origin of state changes

State-change paths like Server Actions or POST routes are protected in two stages, verifying the Origin in addition to a SameSite Cookie.

// 状態変更リクエストは Origin を検証する
const origin = req.headers.get("origin");
const allowed = new URL(process.env.NEXT_PUBLIC_SITE_URL!).origin;
if (req.method !== "GET" && origin !== allowed) {
  return new Response("Forbidden", { status: 403 });
}

This far is horizontal controls. All of them can be taken over just by "choosing the right library/config," requiring no app-specific knowledge. That's exactly why this is the domain to have CI stand guard, not the human brain.


4. Injection classes (the layer detectable with static analysis) — follow tainted input

Injection occurs when "input the client can manipulate (tainted input / source) reaches dangerous processing (a sink) without being validated." Because it has a common structure, you can follow it mechanically not with regular expressions but with data-flow analysis (taint analysis).

Tainted input (source)Dangerous sink (sink)Vulnerability class
searchParams.get("q")String-concatenated SQL / rpc()SQL injection
searchParams.get("url")fetch(url)SSRF
params.filefs.readFile(path)Path traversal
searchParams.get("next")redirect(next)Open redirect
A request-derived stringdangerouslySetInnerHTMLDOM / stored XSS

SQL injection: don't assemble a string, pass it as a value

Supabase's structured API (.eq(), etc.) treats values as parameters and is basically safe, but .or() that receives a raw filter string, and string-concatenated SQL inside rpc() become injection paths.

// 脆弱:ユーザー入力を .or() のフィルタ文字列に直接連結(PostgRESTフィルタ injection)
const q = new URL(req.url).searchParams.get("q") ?? ""; // ← 汚染入力
const { data } = await supabase
  .from("posts")
  .select("*")
  .or(`title.ilike.%${q}%`); // q に区切り文字を仕込むと条件を崩し、意図しない行を引ける

// 修正:構造化APIで「値」として渡す(フィルタ文字列を組み立てない)
const { data: safe } = await supabase
  .from("posts")
  .select("*")
  .ilike("title", `%${q}%`); // q はパラメータ扱いになる

Building dynamic SQL like EXECUTE 'select ... ' || input inside a SECURITY DEFINER function is the same hole. The iron rule is to use format(..., %L) or parameter binding, and not string-concatenate input.

SSRF: the server reaches a user-specified URL

// 脆弱:汚染入力がそのまま fetch のシンクに届く(SSRF)
export async function GET(req: Request) {
  const url = new URL(req.url).searchParams.get("url")!; // ← 汚染入力
  const res = await fetch(url); // ← 危険シンク:169.254.169.254 等の内部資源に到達しうる
  return new Response(await res.text());
}

The fix is "an allowlist of permitted hosts + blocking private/link-local IP ranges + disabling redirect following." In Supabase's server environment, reaching internal metadata or other services is fatal.

Open redirect: trusting the return destination as-is

// 脆弱:next をそのまま信じてフィッシングへ誘導される
const next = new URL(req.url).searchParams.get("next") ?? "/";
redirect(next); // next="https://evil.example/login" でも飛ぶ

// 修正:相対パスだけ許可する("//" 始まりはプロトコル相対なので除外)
redirect(next.startsWith("/") && !next.startsWith("//") ? next : "/");

Path traversal: escaping the file tree with ../

Concatenate user input into a path and read a file, and ../../ reaches outside the intended directory (.env or key files).

// 脆弱:汚染入力をそのままパスに連結(パストラバーサル)
const name = new URL(req.url).searchParams.get("file")!; // ← "../../.env" など
const buf = await fs.readFile(path.join("./uploads", name)); // 危険シンク

// 修正:basename で剥がし、解決後のパスが基底配下にあることを検証する
const base = path.resolve("./uploads");
const resolved = path.resolve(base, path.basename(name));
if (!resolved.startsWith(base + path.sep)) throw new Error("invalid path");
const safe = await fs.readFile(resolved);

DOM XSS is the same type. Pass a request-derived value to dangerouslySetInnerHTML and it executes (it's a sink to limit to trusted output, like server-computed JSON-LD).

The good thing about injection classes is that if you follow the path from the tainted source to the dangerous sink, you can detect it without the app's business knowledge. Unlike the next section's vertical risks, here a tool shows its true ability.


5. Vertical risks (the layer only design can close) — RLS and authorization

This is the core of this article. Because vertical risks depend on the app-specific meaning of "who owns what," neither a library, nor config, nor a WAF can take them over.

5-1. Why a WAF or headers can't prevent them

Let me take IDOR (OWASP API1:2023 Broken Object Level Authorization) as an example. If just rewriting GET /api/invoices/1024 to /api/invoices/1025 returns someone else's invoice, that's IDOR. BOLA has been #1 in the OWASP API Security Top 10 ever since the first edition — the most frequent risk.

This request is perfectly normal as HTTP. The auth header is correct, and it contains no malicious string. From a WAF's viewpoint, it's a "legitimate request from a legitimate user." It becomes an attack because of the app-specific business fact that "#1025 is not owned by this user" — and the WAF doesn't know your data model. The mechanism by which IDOR arises and the fix are dug into in the IDOR / Broken-Authorization Detection Guide.

5-2. RLS misconfiguration — "put it on" and "it's taking effect" are different

Supabase can enforce authorization from the data layer with PostgreSQL's row-level security (RLS) (Supabase: Row Level Security / PostgreSQL: Row Security Policies). It's powerful, but "enabled it" and "taking effect correctly" are different things. Let me list common misconfigurations.

-- アンチパターン集
using ( true )                              -- 無条件許可=実質RLSなし
-- WITH CHECK の無い書き込みポリシー        -- INSERT/UPDATE で他人の行を作れる
-- SECURITY DEFINER 関数で search_path 未固定 -- 権限昇格の温床
-- anon ロールへの過剰な GRANT              -- 公開鍵で書き込めてしまう

The systematic detection of these is summarized in Detecting and Auditing RLS Misconfigurations, and robust policy design in production multi-tenant in Production RLS Multi-Tenancy Design.

5-3. Tenant isolation — get the "value you base it on" wrong and the whole company leaks

The most frightening is the case where the policy is syntactically correct but the value it bases on is user-manipulable. Supabase's JWT has user_metadata (the user themselves can update) and app_metadata (only the server side can write).

-- 危険:user_metadata はユーザー自身が書き換えられる(クライアント操作可能)
create policy "tenant read"
on documents for select
to authenticated
using ( tenant_id = (auth.jwt() -> 'user_metadata' ->> 'tenant_id')::uuid );
-- → 攻撃者が自分の user_metadata.tenant_id を他社IDに書き換えれば、他テナントの行が見える

-- 修正:サーバーだけが書ける app_metadata を根拠にする
create policy "tenant read"
on documents for select
to authenticated
using ( tenant_id = (auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid );

This difference is hard to see through with a tool's syntax check, and is the design judgment itself of "what decides tenant attribution in this system." The verification procedure for cross-tenant leaks is carved out into Verifying Cross-Tenant Leaks in Multi-Tenant.

5-4. service_role "leaps over" RLS

Finally, even if RLS is perfect, there's a path that disables it all. The service_role key runs with PostgreSQL's BYPASSRLS privilege and completely ignores RLS. Use service_role server-side "because it reliably works" and forget to write the ownership check (.eq("user_id", user.id)), and IDOR opens regardless of whether RLS exists. The defense boundary moves from "is there RLS" to "where you place service_role and where you enforce ownership."

// 脆弱:service_role でRLSを飛び越え、所有権チェックを忘れている(IDOR)
const supabaseAdmin = createClient(url, process.env.SUPABASE_SERVICE_ROLE_KEY!);

export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params; // ← クライアントが自由に変えられる
  const { data } = await supabaseAdmin.from("invoices").select("*").eq("id", id).single();
  return Response.json(data); // RLSが完璧でも、service_role が飛び越えるので他人の行が返る
}

// 修正:service_role を使うなら、誰なのかを確定し所有権を必ず WHERE で縛る
const user = await getAuthenticatedUser(req);
if (!user) return new Response("unauthorized", { status: 401 });
const { data } = await supabaseAdmin
  .from("invoices").select("*")
  .eq("id", id).eq("user_id", user.id).single(); // ← この1行が無いとIDOR

The principle is "don't use service_role in the first place; run with the user's session (the anon key) and leave authorization to RLS." Only on paths with a reason to bypass, enforce ownership as above and watch them intensively in review (details in the IDOR guide above).

5-5. Business-logic flaws — allowing "impossible states"

The last of the vertical risks is the class where an authorized legitimate user does something illegitimate with business-impossible input. This can't be prevented by RLS or headers, and only a human with domain knowledge can define it.

// 例:クライアントから送られた価格・数量をそのまま信じる
const { price, qty } = await req.json();
const total = price * qty; // price=0、qty=-1、過去の価格…をそのまま受理してしまう

"Recalculate the amount on the server," "constrain the quantity to a positive integer," "force the order of state transitions (draft → billed → paid)" — such rules are the spec, and can't be derived from the code's external form. That's exactly why it's the domain where automation's limits show most sharply.


6. The 3 detection layers — correlate SAST / RLS verification / DAST

Once you've decided to close it with design, verify "is it closed." Don't rely on one means; layer static, structural, and dynamic.

Layer 1: Static analysis (SAST) — follow the code's data flow

With intra-function taint analysis, follow whether tainted input reaches a DB query or dangerous sink "without being narrowed by ownership." It's a detection you can't write with regular expressions. My published OSS Aegis implements this and runs with no installation.

# インストール不要・設定不要でスキャン(汚染入力→危険シンクを可視化)
npx @aegiskit/cli scan

Layer 2: SQL / RLS verification — does authorization live correctly "in the DB"

Separately from the code, read supabase/migrations/**.sql and sweep out RLS-disabled tables, using (true), missing WITH CHECK, SECURITY DEFINER without a fixed search_path, and over-GRANTs to anon. Further, cross-check SQL and code, and point out "the place where a table with weak RLS is actually queried from a non-admin client" as a confirmed exposure.

Layer 3: Dynamic confirmation (DAST) — actually hit it "as someone else"

Static analysis goes as far as "suspecting." Finally, reproduce IDOR / tenant crossing on an app you own to confirm it. Prepare 2 identities (A and B), and hit B's resource while staying in A's session — if 200 returns, it's confirmed at runtime.

# 自分のアプリへの安全・非破壊なプローブ(所有権の食い違いを実行時に確認)
npx @aegiskit/cli probe http://localhost:3000 --correlate

What matches between the static "suspicion" and the dynamic "reproduction" is fixed first as confirmed-exploitable — this is the SAST↔DAST correlation. To prevent regression, write RLS regression tests with pgTAP and continuously prove "other people's rows aren't visible" in CI.

-- pgTAP:別ユーザーのJWTで他人の行が見えないことを回帰テストにする
begin;
select plan(1);
set local role authenticated;
set local request.jwt.claims to '{"sub":"user-b-uuid"}';
select is_empty(
  $$ select * from invoices where user_id = 'user-a-uuid' $$,
  'user B cannot read user A invoices'
);
select * from finish();
rollback;

The honest scope. No static or dynamic tool proves your authorization is correct. What it looks at is the "shape" of the policy or implementation, not the "meaning" of the business rules or data model. Data-flow analysis is intra-function (intraprocedural) by default and misses flows spanning modules or the framework. A clean result means "you didn't step on the common traps," not "it's safe." These verifications don't replace human review and threat modeling; they complement them.


7. The defense-in-depth map — which layer, who protects

Let me bundle the above onto one page. What matters is that the deeper the layer, the more "the protecting subject" moves from the platform to the human.

LayerMain threatsCountermeasureWho protects
Network / edgeDDoS, bots, known malicious IPsWAF, rate limiting, bot countermeasuresPlatform / config
HTTP / browserXSS, clickjacking, MIME sniffingCSP (nonce), various security headersLibrary / middleware
Input boundarySQLi, SSRF, path traversal, malformed dataZod validation, safe APIs, allowlistDeveloper + static analysis
AuthenticationImpersonation, session fixationSupabase Auth, SameSite CookieLibrary + config
Authorization (data)IDOR/BOLA, tenant crossingRLS + code ownership checksHuman design
Data layerRLS bypass, key leakageanon-key operation, service_role isolation, pgTAPHuman design + verification

The upper half (network ~ authentication) is horizontal controls and injection classes, fixed uniformly with config and libraries. The lower half (authorization, data layer) is vertical risks, protectable only with the two wheels of design and verification. The reason "you're safe because you put in a product" doesn't hold is that the most serious risk is in the place hardest to automate.


8. Pre-production checklist

Whether outsourced or AI-made, before shipping to production, confirm at minimum just this. I list the viewpoint and the danger sign together.

  • RLS enabled on all tables, with policies explicit (just enabling defaults to deny-all = fail-secure)
  • The service_role key is server-only. Prevent mixing with server-only, and don't expose it with NEXT_PUBLIC_
  • Paths using service_role always bind ownership with WHERE (.eq("user_id", user.id))
  • Zod-validate env at startup, and don't put secrets in the client bundle
  • State-change APIs have Origin verification + rate limiting (shared store)
  • Attach CSP (nonce) and the main security headers
  • Tainted input doesn't pass straight through to injection sinks (fetch/redirect/fs/raw SQL)
  • Tenant isolation bases on a server-privilege value like app_metadata (not user_metadata)
  • Confirmed ID swapping / tenant crossing with 2 accounts at runtime
  • SAST / RLS verification / regression tests (pgTAP) permanently installed in CI

From the orderer's viewpoint, the most effective are the 3 questions "what happens if I hit someone else's ID?" "where do you use the service_role key?" "what do you base tenant attribution on?" A good developer can answer immediately.


9. How far yourself, from where an audit

Finally, let me draw the line honestly.

Horizontal controls (Section 3) and injection classes (Section 4) can be crushed mechanically, for the most part, with automation. Choose the right library and config, put static analysis in CI, and a human doesn't need to think every time. First, visualizing the current state with Aegis (free OSS, npx @aegiskit/cli scan) is the most cost-effective first step.

On the other hand, even though you can automate the "detection and warning" of vertical risks (Section 5), the "fix = design" is the human domain. The correctness of authorization, the grounds for tenant attribution, and the validity of business logic can only be judged by a human who understands your data model and business rules. A product that asserts "it's safe" here is rather dangerous. Aegis helps the implementation of horizontal controls and detects/warns on vertical risks, but it doesn't prove that authorization is correct. There's no magic that makes you completely safe.

That's exactly why a line is needed. How far to fix yourself, and from where an expert's review is needed — I organized those criteria in The Scope Where a Security Audit Becomes Necessary. If you need a design fix of vertical risks, or an authorization/RLS review of an existing app, I undertake it with a security audit. I myself have designed and verified app-layer authorization, including RLS, tenant isolation, and ownership enforcement, in real operation on the lumber-distribution-industry DX project.


Frequently Asked Questions (FAQ)

Q. What should I tackle first? A. There's an order. (1) Enable RLS on all tables and isolate service_role (the vertical foundation), (2) visualize injection and horizontal-control holes with npx @aegiskit/cli scan, (3) confirm ID swapping with 2 accounts. These 3 prevent the biggest accidents at the minimum cost.

Q. Is it enough to put in a WAF and security headers? A. Not enough. As in Section 7, those only take effect on the upper half (horizontal controls), and the most serious authorization (IDOR, tenant crossing) is a "legitimate request" so they can't prevent it. Both are needed, but one doesn't substitute for the other.

Q. Will it be fixed if I ask AI to "write it securely"? A. Don't over-expect. In Veracode's study, security scores stayed flat even as models got smarter. AI is strong at generating "code that works," but doesn't guarantee a "structure that doesn't break." Only by passing it through verification gates (scan, test, review) does it become production quality.

Q. Is authorization safe just by putting on RLS? A. No. The service_role path leaps over RLS, and domains where it doesn't take effect remain — SECURITY DEFINER functions, RPC, Storage, external join targets, etc. Furthermore, get the value you base on wrong (user_metadata or app_metadata), and even syntactically correct RLS leaks the whole company. RLS is the mandatory "last bastion," but the correct answer is to protect in two layers with code-side ownership checks.

Q. Even for solo or small-scale, should I do this far? A. Rather, the reality that CVE-2025-48757 and the like show is that apps built quickly with AI have more exposure cases. At minimum, be sure to do just the 3 points: "RLS on all tables," "service_role isolation + ownership checks," and "ID-swapping confirmation with 2 accounts." The cost is slight, and the accidents you can prevent are orders of magnitude larger.


Summary: with a map, the priority of defense is decided

Let me organize the key points.

  • Draw the map of app-layer security by dividing it into the 3 layers horizontal controls, injection classes, and vertical risks. Crush the upper 2 with automation, and protect the lowest with design and verification.
  • Horizontal controls (headers/CSP, rate limiting, CSRF, Zod, env, secret hygiene) can be taken over uniformly with the right library and config. The crux of serverless rate limiting is putting the state in a shared store.
  • Injection classes (SQLi, SSRF, path traversal, open redirect, DOM XSS) can be detected mechanically with static analysis (taint) that follows the data flow of "tainted input → dangerous sink."
  • Vertical risks (authorization/IDOR, RLS design, tenant isolation, business logic) appear as a "legitimate request" with correct authentication and format, so a WAF or headers can't prevent them. The defense boundary moves from "whether RLS exists" to "where you place the key and ownership" and "the grounds for tenant attribution."
  • Correlate detection across the 3 layers of SAST / RLS verification / DAST. But a tool only helps discovery, and the correctness of authorization can only be protected by design and human review.

Building fast with AI is itself correct. Hardening what you built fast safely, without leaking — if you need that mechanism-building, or an authorization/RLS review of an existing Next.js × Supabase app, feel free to consult us.


References

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

The vulnerabilities in this article — is your app safe from them?

An expert audit of your Next.js × Supabase authorization & RLS

The IDOR, RLS misconfigurations, and tenant-boundary crossing covered here are vertical risks a library can't fix. I take it on as a security audit — from authorization review through fix design and implementation. You're welcome to visualize the current state with the free OSS first.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading