[{"data":1,"prerenderedAt":1337},["ShallowReactive",2],{"blog:\u002Fblog\u002Fnuxt\u002Fsupabase-function-security":3},{"id":4,"title":5,"author":6,"body":7,"category":1320,"date":1321,"description":1322,"draft":1323,"extension":1324,"image":1325,"meta":1326,"navigation":99,"path":1327,"seo":1328,"series":1329,"seriesOrder":109,"seriesTitle":1330,"stem":1331,"tags":1332,"updatedAt":1325,"__hash__":1336},"blog\u002Fblog\u002Fnuxt\u002Fsupabase-function-security\u002Findex.md","Database Function 安全規範","charles",{"type":8,"value":9,"toc":1286},"minimark",[10,14,18,42,45,49,53,155,158,165,180,188,190,194,198,206,217,293,297,302,313,377,380,430,432,436,440,447,495,499,505,507,510,513,546,549,616,619,655,657,660,663,821,825,937,941,1008,1010,1013,1017,1026,1038,1044,1052,1056,1064,1117,1121,1126,1131,1138,1140,1143,1146,1201,1203,1206,1245,1247,1250,1282],[11,12,13],"h2",{"id":13},"這篇要解決什麼問題",[15,16,17],"p",{},"Database Function 是處理複雜業務邏輯的利器，但也是安全漏洞的溫床。這篇文章將說明：",[19,20,21,29,32,39],"ul",{},[22,23,24,28],"li",{},[25,26,27],"code",{},"SET search_path = ''"," 為什麼是強制要求",[22,30,31],{},"SECURITY DEFINER vs SECURITY INVOKER 的選擇",[22,33,34,35,38],{},"View 的 ",[25,36,37],{},"security_invoker = true"," 設定",[22,40,41],{},"supabase db lint 安全檢查",[43,44],"hr",{},[11,46,48],{"id":47},"search_path-安全漏洞","search_path 安全漏洞",[50,51,52],"h3",{"id":52},"為什麼必須設為空字串",[54,55,60],"pre",{"className":56,"code":57,"language":58,"meta":59,"style":59},"language-sql shiki shiki-themes material-theme-lighter github-light github-dark","-- ❌ 危險 - 可能被 schema 注入攻擊\nCREATE FUNCTION get_user()\nRETURNS text AS $$\n  SELECT name FROM users WHERE id = auth.uid();\n$$ LANGUAGE sql;\n\n-- ✅ 安全 - 使用完整 schema 名稱\nCREATE FUNCTION public.get_user()\nRETURNS text\nLANGUAGE sql\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n  SELECT name FROM public.users WHERE id = auth.uid();\n$$;\n","sql","",[25,61,62,70,76,82,88,94,101,107,113,119,125,131,137,143,149],{"__ignoreMap":59},[63,64,67],"span",{"class":65,"line":66},"line",1,[63,68,69],{},"-- ❌ 危險 - 可能被 schema 注入攻擊\n",[63,71,73],{"class":65,"line":72},2,[63,74,75],{},"CREATE FUNCTION get_user()\n",[63,77,79],{"class":65,"line":78},3,[63,80,81],{},"RETURNS text AS $$\n",[63,83,85],{"class":65,"line":84},4,[63,86,87],{},"  SELECT name FROM users WHERE id = auth.uid();\n",[63,89,91],{"class":65,"line":90},5,[63,92,93],{},"$$ LANGUAGE sql;\n",[63,95,97],{"class":65,"line":96},6,[63,98,100],{"emptyLinePlaceholder":99},true,"\n",[63,102,104],{"class":65,"line":103},7,[63,105,106],{},"-- ✅ 安全 - 使用完整 schema 名稱\n",[63,108,110],{"class":65,"line":109},8,[63,111,112],{},"CREATE FUNCTION public.get_user()\n",[63,114,116],{"class":65,"line":115},9,[63,117,118],{},"RETURNS text\n",[63,120,122],{"class":65,"line":121},10,[63,123,124],{},"LANGUAGE sql\n",[63,126,128],{"class":65,"line":127},11,[63,129,130],{},"SECURITY DEFINER\n",[63,132,134],{"class":65,"line":133},12,[63,135,136],{},"SET search_path = ''\n",[63,138,140],{"class":65,"line":139},13,[63,141,142],{},"AS $$\n",[63,144,146],{"class":65,"line":145},14,[63,147,148],{},"  SELECT name FROM public.users WHERE id = auth.uid();\n",[63,150,152],{"class":65,"line":151},15,[63,153,154],{},"$$;\n",[50,156,157],{"id":157},"攻擊原理",[15,159,160,161,164],{},"如果不設定 ",[25,162,163],{},"search_path = ''","，攻擊者可能：",[166,167,168,171,174,177],"ol",{},[22,169,170],{},"建立一個同名的惡意 schema",[22,172,173],{},"在其中放置同名的惡意 table 或 function",[22,175,176],{},"你的 function 可能會存取到惡意物件",[22,178,179],{},"導致資料外洩或權限提升",[54,181,186],{"className":182,"code":184,"language":185},[183],"language-text","┌──────────────────────────────────────────────────┐\n│  search_path = 'public, pg_temp'                 │\n│  → SELECT * FROM users                           │\n│  → 可能存取到 pg_temp.users（攻擊者建立的）      │\n└──────────────────────────────────────────────────┘\n\n┌──────────────────────────────────────────────────┐\n│  search_path = ''                                │\n│  → SELECT * FROM public.users                    │\n│  → 一定存取到 public.users                       │\n└──────────────────────────────────────────────────┘\n","text",[25,187,184],{"__ignoreMap":59},[43,189],{},[11,191,193],{"id":192},"security-definer-vs-invoker","SECURITY DEFINER vs INVOKER",[50,195,197],{"id":196},"security-definer","SECURITY DEFINER",[15,199,200,201,205],{},"以",[202,203,204],"strong",{},"函式建立者","的權限執行，適用於：",[19,207,208,211,214],{},[22,209,210],{},"需要存取使用者無權存取的資料",[22,212,213],{},"實作跨使用者的管理功能",[22,215,216],{},"封裝敏感邏輯",[54,218,220],{"className":56,"code":219,"language":58,"meta":59,"style":59},"-- 以建立者權限執行（通常是 postgres 超級用戶）\nCREATE FUNCTION public.admin_get_all_users()\nRETURNS SETOF public.users\nLANGUAGE plpgsql\nSECURITY DEFINER  -- 使用 DEFINER\nSET search_path = ''\nAS $$\nBEGIN\n  -- 即使呼叫者沒有 SELECT 權限，也能執行\n  RETURN QUERY SELECT * FROM public.users;\nEND;\n$$;\n\n-- 限制誰可以呼叫\nGRANT EXECUTE ON FUNCTION public.admin_get_all_users TO service_role;\n",[25,221,222,227,232,237,242,247,251,255,260,265,270,275,279,283,288],{"__ignoreMap":59},[63,223,224],{"class":65,"line":66},[63,225,226],{},"-- 以建立者權限執行（通常是 postgres 超級用戶）\n",[63,228,229],{"class":65,"line":72},[63,230,231],{},"CREATE FUNCTION public.admin_get_all_users()\n",[63,233,234],{"class":65,"line":78},[63,235,236],{},"RETURNS SETOF public.users\n",[63,238,239],{"class":65,"line":84},[63,240,241],{},"LANGUAGE plpgsql\n",[63,243,244],{"class":65,"line":90},[63,245,246],{},"SECURITY DEFINER  -- 使用 DEFINER\n",[63,248,249],{"class":65,"line":96},[63,250,136],{},[63,252,253],{"class":65,"line":103},[63,254,142],{},[63,256,257],{"class":65,"line":109},[63,258,259],{},"BEGIN\n",[63,261,262],{"class":65,"line":115},[63,263,264],{},"  -- 即使呼叫者沒有 SELECT 權限，也能執行\n",[63,266,267],{"class":65,"line":121},[63,268,269],{},"  RETURN QUERY SELECT * FROM public.users;\n",[63,271,272],{"class":65,"line":127},[63,273,274],{},"END;\n",[63,276,277],{"class":65,"line":133},[63,278,154],{},[63,280,281],{"class":65,"line":139},[63,282,100],{"emptyLinePlaceholder":99},[63,284,285],{"class":65,"line":145},[63,286,287],{},"-- 限制誰可以呼叫\n",[63,289,290],{"class":65,"line":151},[63,291,292],{},"GRANT EXECUTE ON FUNCTION public.admin_get_all_users TO service_role;\n",[50,294,296],{"id":295},"security-invoker","SECURITY INVOKER",[15,298,200,299,205],{},[202,300,301],{},"呼叫者",[19,303,304,307,310],{},[22,305,306],{},"一般業務邏輯",[22,308,309],{},"需要遵守 RLS 的操作",[22,311,312],{},"更安全的預設選擇",[54,314,316],{"className":56,"code":315,"language":58,"meta":59,"style":59},"-- 以呼叫者權限執行\nCREATE FUNCTION public.get_my_profile()\nRETURNS public.users\nLANGUAGE plpgsql\nSECURITY INVOKER  -- 使用 INVOKER\nSET search_path = ''\nAS $$\nBEGIN\n  -- 會受到呼叫者的 RLS 限制\n  RETURN QUERY\n  SELECT * FROM public.users WHERE id = auth.uid();\nEND;\n$$;\n",[25,317,318,323,328,333,337,342,346,350,354,359,364,369,373],{"__ignoreMap":59},[63,319,320],{"class":65,"line":66},[63,321,322],{},"-- 以呼叫者權限執行\n",[63,324,325],{"class":65,"line":72},[63,326,327],{},"CREATE FUNCTION public.get_my_profile()\n",[63,329,330],{"class":65,"line":78},[63,331,332],{},"RETURNS public.users\n",[63,334,335],{"class":65,"line":84},[63,336,241],{},[63,338,339],{"class":65,"line":90},[63,340,341],{},"SECURITY INVOKER  -- 使用 INVOKER\n",[63,343,344],{"class":65,"line":96},[63,345,136],{},[63,347,348],{"class":65,"line":103},[63,349,142],{},[63,351,352],{"class":65,"line":109},[63,353,259],{},[63,355,356],{"class":65,"line":115},[63,357,358],{},"  -- 會受到呼叫者的 RLS 限制\n",[63,360,361],{"class":65,"line":121},[63,362,363],{},"  RETURN QUERY\n",[63,365,366],{"class":65,"line":127},[63,367,368],{},"  SELECT * FROM public.users WHERE id = auth.uid();\n",[63,370,371],{"class":65,"line":133},[63,372,274],{},[63,374,375],{"class":65,"line":139},[63,376,154],{},[50,378,379],{"id":379},"選擇建議",[381,382,383,396],"table",{},[384,385,386],"thead",{},[387,388,389,393],"tr",{},[390,391,392],"th",{},"場景",[390,394,395],{},"建議",[397,398,399,407,415,422],"tbody",{},[387,400,401,405],{},[402,403,404],"td",{},"一般查詢\u002F更新",[402,406,296],{},[387,408,409,412],{},[402,410,411],{},"管理員功能",[402,413,414],{},"SECURITY DEFINER + 權限檢查",[387,416,417,420],{},[402,418,419],{},"跨使用者操作",[402,421,414],{},[387,423,424,427],{},[402,425,426],{},"系統維護",[402,428,429],{},"SECURITY DEFINER + 僅限 service_role",[43,431],{},[11,433,435],{"id":434},"view-的-security_invoker-設定","View 的 security_invoker 設定",[50,437,439],{"id":438},"為什麼-view-需要特別處理","為什麼 View 需要特別處理",[15,441,442,443,446],{},"View 預設以",[202,444,445],{},"建立者權限","執行，可能繞過 RLS：",[54,448,450],{"className":56,"code":449,"language":58,"meta":59,"style":59},"-- ❌ 危險 - 可能繞過 RLS\nCREATE VIEW public.user_summary AS\nSELECT id, name, email FROM public.users;\n\n-- ✅ 安全 - 明確使用呼叫者權限\nCREATE VIEW public.user_summary\nWITH (security_invoker = true)\nAS\nSELECT id, name, email FROM public.users;\n",[25,451,452,457,462,467,471,476,481,486,491],{"__ignoreMap":59},[63,453,454],{"class":65,"line":66},[63,455,456],{},"-- ❌ 危險 - 可能繞過 RLS\n",[63,458,459],{"class":65,"line":72},[63,460,461],{},"CREATE VIEW public.user_summary AS\n",[63,463,464],{"class":65,"line":78},[63,465,466],{},"SELECT id, name, email FROM public.users;\n",[63,468,469],{"class":65,"line":84},[63,470,100],{"emptyLinePlaceholder":99},[63,472,473],{"class":65,"line":90},[63,474,475],{},"-- ✅ 安全 - 明確使用呼叫者權限\n",[63,477,478],{"class":65,"line":96},[63,479,480],{},"CREATE VIEW public.user_summary\n",[63,482,483],{"class":65,"line":103},[63,484,485],{},"WITH (security_invoker = true)\n",[63,487,488],{"class":65,"line":109},[63,489,490],{},"AS\n",[63,492,493],{"class":65,"line":115},[63,494,466],{},[50,496,498],{"id":497},"security_invoker-true-的效果","security_invoker = true 的效果",[54,500,503],{"className":501,"code":502,"language":185},[183],"呼叫 VIEW\n    │\n    ▼ security_invoker = false（預設）\n┌────────────────────────────────────────┐\n│  以 VIEW 建立者權限執行                  │\n│  → 繞過呼叫者的 RLS 限制                 │\n│  → 可能看到不該看的資料                  │\n└────────────────────────────────────────┘\n\n    ▼ security_invoker = true\n┌────────────────────────────────────────┐\n│  以呼叫者權限執行                        │\n│  → 遵守呼叫者的 RLS 限制                 │\n│  → 只能看到有權限的資料                  │\n└────────────────────────────────────────┘\n",[25,504,502],{"__ignoreMap":59},[43,506],{},[11,508,41],{"id":509},"supabase-db-lint-安全檢查",[50,511,512],{"id":512},"必須零警告",[54,514,518],{"className":515,"code":516,"language":517,"meta":59,"style":59},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","# 執行安全檢查\nsupabase db lint --level warning\n","bash",[25,519,520,526],{"__ignoreMap":59},[63,521,522],{"class":65,"line":66},[63,523,525],{"class":524},"sutJx","# 執行安全檢查\n",[63,527,528,532,536,539,543],{"class":65,"line":72},[63,529,531],{"class":530},"sbgvK","supabase",[63,533,535],{"class":534},"s_sjI"," db",[63,537,538],{"class":534}," lint",[63,540,542],{"class":541},"stzsN"," --level",[63,544,545],{"class":534}," warning\n",[50,547,548],{"id":548},"常見警告與修正",[381,550,551,564],{},[384,552,553],{},[387,554,555,558,561],{},[390,556,557],{},"警告",[390,559,560],{},"原因",[390,562,563],{},"修正",[397,565,566,583,601],{},[387,567,568,573,578],{},[402,569,570],{},[25,571,572],{},"function_search_path_mutable",[402,574,575,576],{},"函式缺少 ",[25,577,27],{},[402,579,580,581],{},"加入 ",[25,582,27],{},[387,584,585,590,596],{},[402,586,587],{},[25,588,589],{},"security_invoker_not_set",[402,591,592,593],{},"View 缺少 ",[25,594,595],{},"security_invoker",[402,597,580,598],{},[25,599,600],{},"WITH (security_invoker = true)",[387,602,603,608,611],{},[402,604,605],{},[25,606,607],{},"rls_disabled",[402,609,610],{},"表沒有啟用 RLS",[402,612,613],{},[25,614,615],{},"ALTER TABLE ... ENABLE ROW LEVEL SECURITY",[50,617,618],{"id":618},"提交前檢查",[54,620,622],{"className":515,"code":621,"language":517,"meta":59,"style":59},"# 提交前必須執行\nsupabase db lint --level warning\n\n# 如果有警告，修正後重新檢查\n# 絕不提交有警告的 migration\n",[25,623,624,629,641,645,650],{"__ignoreMap":59},[63,625,626],{"class":65,"line":66},[63,627,628],{"class":524},"# 提交前必須執行\n",[63,630,631,633,635,637,639],{"class":65,"line":72},[63,632,531],{"class":530},[63,634,535],{"class":534},[63,636,538],{"class":534},[63,638,542],{"class":541},[63,640,545],{"class":534},[63,642,643],{"class":65,"line":78},[63,644,100],{"emptyLinePlaceholder":99},[63,646,647],{"class":65,"line":84},[63,648,649],{"class":524},"# 如果有警告，修正後重新檢查\n",[63,651,652],{"class":65,"line":90},[63,653,654],{"class":524},"# 絕不提交有警告的 migration\n",[43,656],{},[11,658,659],{"id":659},"函式模板",[50,661,662],{"id":662},"標準安全函式模板",[54,664,666],{"className":56,"code":665,"language":58,"meta":59,"style":59},"CREATE OR REPLACE FUNCTION public.my_function(\n  p_param1 uuid,\n  p_param2 text DEFAULT NULL\n)\nRETURNS TABLE (id uuid, name text)\nLANGUAGE plpgsql\nSECURITY DEFINER\nSET search_path = ''\nAS $$\nBEGIN\n  -- 1. 權限檢查（如需要）\n  IF public.current_user_role() NOT IN ('admin', 'manager') THEN\n    RAISE EXCEPTION 'Permission denied';\n  END IF;\n\n  -- 2. 參數驗證（如需要）\n  IF p_param1 IS NULL THEN\n    RAISE EXCEPTION 'param1 is required';\n  END IF;\n\n  -- 3. 業務邏輯（使用完整 schema 名稱）\n  RETURN QUERY\n  SELECT t.id, t.name\n  FROM public.some_table t\n  WHERE t.param = p_param1;\nEND;\n$$;\n\n-- 4. 設定權限（限制誰可以呼叫）\nGRANT EXECUTE ON FUNCTION public.my_function TO authenticated;\n",[25,667,668,673,678,683,688,693,697,701,705,709,713,718,723,728,733,737,743,749,755,760,765,771,776,782,788,794,799,804,809,815],{"__ignoreMap":59},[63,669,670],{"class":65,"line":66},[63,671,672],{},"CREATE OR REPLACE FUNCTION public.my_function(\n",[63,674,675],{"class":65,"line":72},[63,676,677],{},"  p_param1 uuid,\n",[63,679,680],{"class":65,"line":78},[63,681,682],{},"  p_param2 text DEFAULT NULL\n",[63,684,685],{"class":65,"line":84},[63,686,687],{},")\n",[63,689,690],{"class":65,"line":90},[63,691,692],{},"RETURNS TABLE (id uuid, name text)\n",[63,694,695],{"class":65,"line":96},[63,696,241],{},[63,698,699],{"class":65,"line":103},[63,700,130],{},[63,702,703],{"class":65,"line":109},[63,704,136],{},[63,706,707],{"class":65,"line":115},[63,708,142],{},[63,710,711],{"class":65,"line":121},[63,712,259],{},[63,714,715],{"class":65,"line":127},[63,716,717],{},"  -- 1. 權限檢查（如需要）\n",[63,719,720],{"class":65,"line":133},[63,721,722],{},"  IF public.current_user_role() NOT IN ('admin', 'manager') THEN\n",[63,724,725],{"class":65,"line":139},[63,726,727],{},"    RAISE EXCEPTION 'Permission denied';\n",[63,729,730],{"class":65,"line":145},[63,731,732],{},"  END IF;\n",[63,734,735],{"class":65,"line":151},[63,736,100],{"emptyLinePlaceholder":99},[63,738,740],{"class":65,"line":739},16,[63,741,742],{},"  -- 2. 參數驗證（如需要）\n",[63,744,746],{"class":65,"line":745},17,[63,747,748],{},"  IF p_param1 IS NULL THEN\n",[63,750,752],{"class":65,"line":751},18,[63,753,754],{},"    RAISE EXCEPTION 'param1 is required';\n",[63,756,758],{"class":65,"line":757},19,[63,759,732],{},[63,761,763],{"class":65,"line":762},20,[63,764,100],{"emptyLinePlaceholder":99},[63,766,768],{"class":65,"line":767},21,[63,769,770],{},"  -- 3. 業務邏輯（使用完整 schema 名稱）\n",[63,772,774],{"class":65,"line":773},22,[63,775,363],{},[63,777,779],{"class":65,"line":778},23,[63,780,781],{},"  SELECT t.id, t.name\n",[63,783,785],{"class":65,"line":784},24,[63,786,787],{},"  FROM public.some_table t\n",[63,789,791],{"class":65,"line":790},25,[63,792,793],{},"  WHERE t.param = p_param1;\n",[63,795,797],{"class":65,"line":796},26,[63,798,274],{},[63,800,802],{"class":65,"line":801},27,[63,803,154],{},[63,805,807],{"class":65,"line":806},28,[63,808,100],{"emptyLinePlaceholder":99},[63,810,812],{"class":65,"line":811},29,[63,813,814],{},"-- 4. 設定權限（限制誰可以呼叫）\n",[63,816,818],{"class":65,"line":817},30,[63,819,820],{},"GRANT EXECUTE ON FUNCTION public.my_function TO authenticated;\n",[50,822,824],{"id":823},"helper-函式模板","Helper 函式模板",[54,826,828],{"className":56,"code":827,"language":58,"meta":59,"style":59},"-- 取得當前使用者角色\nCREATE OR REPLACE FUNCTION public.current_user_role()\nRETURNS text\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n  SELECT COALESCE(\n    (SELECT role FROM public.user_roles WHERE user_id = auth.uid()),\n    'unauthorized'\n  )::text;\n$$;\n\n-- 取得當前使用者 ID\nCREATE OR REPLACE FUNCTION public.current_user_id()\nRETURNS uuid\nLANGUAGE sql\nSTABLE\nSECURITY DEFINER\nSET search_path = ''\nAS $$\n  SELECT auth.uid();\n$$;\n",[25,829,830,835,840,844,848,853,857,861,865,870,875,880,885,889,893,898,903,908,912,916,920,924,928,933],{"__ignoreMap":59},[63,831,832],{"class":65,"line":66},[63,833,834],{},"-- 取得當前使用者角色\n",[63,836,837],{"class":65,"line":72},[63,838,839],{},"CREATE OR REPLACE FUNCTION public.current_user_role()\n",[63,841,842],{"class":65,"line":78},[63,843,118],{},[63,845,846],{"class":65,"line":84},[63,847,124],{},[63,849,850],{"class":65,"line":90},[63,851,852],{},"STABLE\n",[63,854,855],{"class":65,"line":96},[63,856,130],{},[63,858,859],{"class":65,"line":103},[63,860,136],{},[63,862,863],{"class":65,"line":109},[63,864,142],{},[63,866,867],{"class":65,"line":115},[63,868,869],{},"  SELECT COALESCE(\n",[63,871,872],{"class":65,"line":121},[63,873,874],{},"    (SELECT role FROM public.user_roles WHERE user_id = auth.uid()),\n",[63,876,877],{"class":65,"line":127},[63,878,879],{},"    'unauthorized'\n",[63,881,882],{"class":65,"line":133},[63,883,884],{},"  )::text;\n",[63,886,887],{"class":65,"line":139},[63,888,154],{},[63,890,891],{"class":65,"line":145},[63,892,100],{"emptyLinePlaceholder":99},[63,894,895],{"class":65,"line":151},[63,896,897],{},"-- 取得當前使用者 ID\n",[63,899,900],{"class":65,"line":739},[63,901,902],{},"CREATE OR REPLACE FUNCTION public.current_user_id()\n",[63,904,905],{"class":65,"line":745},[63,906,907],{},"RETURNS uuid\n",[63,909,910],{"class":65,"line":751},[63,911,124],{},[63,913,914],{"class":65,"line":757},[63,915,852],{},[63,917,918],{"class":65,"line":762},[63,919,130],{},[63,921,922],{"class":65,"line":767},[63,923,136],{},[63,925,926],{"class":65,"line":773},[63,927,142],{},[63,929,930],{"class":65,"line":778},[63,931,932],{},"  SELECT auth.uid();\n",[63,934,935],{"class":65,"line":784},[63,936,154],{},[50,938,940],{"id":939},"view-模板","View 模板",[54,942,944],{"className":56,"code":943,"language":58,"meta":59,"style":59},"CREATE OR REPLACE VIEW public.user_summary\nWITH (security_invoker = true)\nAS\nSELECT\n  u.id,\n  u.name,\n  u.email,\n  ur.role\nFROM public.users u\nLEFT JOIN public.user_roles ur ON u.id = ur.user_id;\n\n-- 設定權限\nGRANT SELECT ON public.user_summary TO authenticated;\n",[25,945,946,951,955,959,964,969,974,979,984,989,994,998,1003],{"__ignoreMap":59},[63,947,948],{"class":65,"line":66},[63,949,950],{},"CREATE OR REPLACE VIEW public.user_summary\n",[63,952,953],{"class":65,"line":72},[63,954,485],{},[63,956,957],{"class":65,"line":78},[63,958,490],{},[63,960,961],{"class":65,"line":84},[63,962,963],{},"SELECT\n",[63,965,966],{"class":65,"line":90},[63,967,968],{},"  u.id,\n",[63,970,971],{"class":65,"line":96},[63,972,973],{},"  u.name,\n",[63,975,976],{"class":65,"line":103},[63,977,978],{},"  u.email,\n",[63,980,981],{"class":65,"line":109},[63,982,983],{},"  ur.role\n",[63,985,986],{"class":65,"line":115},[63,987,988],{},"FROM public.users u\n",[63,990,991],{"class":65,"line":121},[63,992,993],{},"LEFT JOIN public.user_roles ur ON u.id = ur.user_id;\n",[63,995,996],{"class":65,"line":127},[63,997,100],{"emptyLinePlaceholder":99},[63,999,1000],{"class":65,"line":133},[63,1001,1002],{},"-- 設定權限\n",[63,1004,1005],{"class":65,"line":139},[63,1006,1007],{},"GRANT SELECT ON public.user_summary TO authenticated;\n",[43,1009],{},[11,1011,1012],{"id":1012},"踩坑經驗",[50,1014,1016],{"id":1015},"search_path-漏洞的真實案例","search_path 漏洞的真實案例",[15,1018,1019,1022,1023,1025],{},[202,1020,1021],{},"問題","：建立函式時沒有設定 ",[25,1024,163],{},"。",[15,1027,1028,1031,1032,1035,1036,1025],{},[202,1029,1030],{},"後果","：",[25,1033,1034],{},"supabase db lint"," 一直報警告 ",[25,1037,572],{},[15,1039,1040,1043],{},[202,1041,1042],{},"解決","：所有函式都要加上：",[54,1045,1046],{"className":56,"code":136,"language":58,"meta":59,"style":59},[25,1047,1048],{"__ignoreMap":59},[63,1049,1050],{"class":65,"line":66},[63,1051,136],{},[50,1053,1055],{"id":1054},"忘記使用完整-schema-名稱","忘記使用完整 schema 名稱",[15,1057,1058,1060,1061,1063],{},[202,1059,1021],{},"：設定了 ",[25,1062,163],{},"，但 SQL 內沒有使用完整名稱。",[54,1065,1067],{"className":56,"code":1066,"language":58,"meta":59,"style":59},"-- ❌ 錯誤：search_path = '' 但沒有 schema 前綴\nSET search_path = ''\nAS $$\n  SELECT * FROM users;  -- 會報錯：relation \"users\" does not exist\n$$;\n\n-- ✅ 正確：使用完整 schema 名稱\nSET search_path = ''\nAS $$\n  SELECT * FROM public.users;  -- 正確\n$$;\n",[25,1068,1069,1074,1078,1082,1087,1091,1095,1100,1104,1108,1113],{"__ignoreMap":59},[63,1070,1071],{"class":65,"line":66},[63,1072,1073],{},"-- ❌ 錯誤：search_path = '' 但沒有 schema 前綴\n",[63,1075,1076],{"class":65,"line":72},[63,1077,136],{},[63,1079,1080],{"class":65,"line":78},[63,1081,142],{},[63,1083,1084],{"class":65,"line":84},[63,1085,1086],{},"  SELECT * FROM users;  -- 會報錯：relation \"users\" does not exist\n",[63,1088,1089],{"class":65,"line":90},[63,1090,154],{},[63,1092,1093],{"class":65,"line":96},[63,1094,100],{"emptyLinePlaceholder":99},[63,1096,1097],{"class":65,"line":103},[63,1098,1099],{},"-- ✅ 正確：使用完整 schema 名稱\n",[63,1101,1102],{"class":65,"line":109},[63,1103,136],{},[63,1105,1106],{"class":65,"line":115},[63,1107,142],{},[63,1109,1110],{"class":65,"line":121},[63,1111,1112],{},"  SELECT * FROM public.users;  -- 正確\n",[63,1114,1115],{"class":65,"line":127},[63,1116,154],{},[50,1118,1120],{"id":1119},"view-忘記設定-security_invoker","View 忘記設定 security_invoker",[15,1122,1123,1125],{},[202,1124,1021],{},"：建立 View 後，普通使用者可以看到所有資料。",[15,1127,1128,1130],{},[202,1129,560],{},"：View 預設以建立者權限執行，繞過了 RLS。",[15,1132,1133,1135,1136,1025],{},[202,1134,1042],{},"：所有 View 都要加上 ",[25,1137,600],{},[43,1139],{},[11,1141,1142],{"id":1142},"檢查清單",[15,1144,1145],{},"提交 Migration 前確認：",[19,1147,1150,1161,1171,1177,1185,1195],{"className":1148},[1149],"contains-task-list",[22,1151,1154,1158,1159],{"className":1152},[1153],"task-list-item",[1155,1156],"input",{"disabled":99,"type":1157},"checkbox"," 所有函式都有 ",[25,1160,27],{},[22,1162,1164,1166,1167,1170],{"className":1163},[1153],[1155,1165],{"disabled":99,"type":1157}," 所有 SQL 都使用完整 schema 名稱（如 ",[25,1168,1169],{},"public.users","）",[22,1172,1174,1176],{"className":1173},[1153],[1155,1175],{"disabled":99,"type":1157}," SECURITY DEFINER 函式有適當的權限檢查",[22,1178,1180,1182,1183],{"className":1179},[1153],[1155,1181],{"disabled":99,"type":1157}," 所有 View 都有 ",[25,1184,600],{},[22,1186,1188,1190,1191,1194],{"className":1187},[1153],[1155,1189],{"disabled":99,"type":1157}," ",[25,1192,1193],{},"supabase db lint --level warning"," 零警告",[22,1196,1198,1200],{"className":1197},[1153],[1155,1199],{"disabled":99,"type":1157}," 已設定適當的 GRANT 權限",[43,1202],{},[11,1204,1205],{"id":1205},"最佳實踐總結",[166,1207,1208,1213,1224,1233,1239],{},[22,1209,1210,1212],{},[202,1211,163],{},"：所有函式都要設定，無例外",[22,1214,1215,1031,1218,1221,1222],{},[202,1216,1217],{},"完整 schema 名稱",[25,1219,1220],{},"public.table"," 而非 ",[25,1223,381],{},[22,1225,1226,1229,1230],{},[202,1227,1228],{},"View security_invoker","：所有 View 都要設定 ",[25,1231,1232],{},"true",[22,1234,1235,1238],{},[202,1236,1237],{},"db lint 零警告","：提交前必須通過檢查",[22,1240,1241,1244],{},[202,1242,1243],{},"最小權限原則","：GRANT 只給需要的角色",[43,1246],{},[11,1248,1249],{"id":1249},"延伸閱讀",[19,1251,1252,1261,1268,1275],{},[22,1253,1254],{},[1255,1256,1260],"a",{"href":1257,"rel":1258},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fsql-createfunction.html",[1259],"nofollow","PostgreSQL Function Security",[22,1262,1263],{},[1255,1264,1267],{"href":1265,"rel":1266},"https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fdatabase\u002Ffunctions",[1259],"Supabase Functions",[22,1269,1270,1271],{},"上一篇：",[1255,1272,1274],{"href":1273},"\u002Fblog\u002Fnuxt\u002Fsupabase-rls-strategy","RLS 與「讀 Client，寫 Server」策略",[22,1276,1277,1278],{},"下一篇：",[1255,1279,1281],{"href":1280},"\u002Fblog\u002Fnuxt\u002Fsupabase-self-hosted","Self-hosted Supabase 部署與遷移",[1283,1284,1285],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":59,"searchDepth":78,"depth":78,"links":1287},[1288,1289,1293,1298,1302,1307,1312,1317,1318,1319],{"id":13,"depth":72,"text":13},{"id":47,"depth":72,"text":48,"children":1290},[1291,1292],{"id":52,"depth":78,"text":52},{"id":157,"depth":78,"text":157},{"id":192,"depth":72,"text":193,"children":1294},[1295,1296,1297],{"id":196,"depth":78,"text":197},{"id":295,"depth":78,"text":296},{"id":379,"depth":78,"text":379},{"id":434,"depth":72,"text":435,"children":1299},[1300,1301],{"id":438,"depth":78,"text":439},{"id":497,"depth":78,"text":498},{"id":509,"depth":72,"text":41,"children":1303},[1304,1305,1306],{"id":512,"depth":78,"text":512},{"id":548,"depth":78,"text":548},{"id":618,"depth":78,"text":618},{"id":659,"depth":72,"text":659,"children":1308},[1309,1310,1311],{"id":662,"depth":78,"text":662},{"id":823,"depth":78,"text":824},{"id":939,"depth":78,"text":940},{"id":1012,"depth":72,"text":1012,"children":1313},[1314,1315,1316],{"id":1015,"depth":78,"text":1016},{"id":1054,"depth":78,"text":1055},{"id":1119,"depth":78,"text":1120},{"id":1142,"depth":72,"text":1142},{"id":1205,"depth":72,"text":1205},{"id":1249,"depth":72,"text":1249},"Nuxt","2026-01-22","撰寫安全的 Supabase Database Function，涵蓋 search_path、SECURITY DEFINER、與 supabase db lint 檢查。",false,"md",null,{},"\u002Fblog\u002Fnuxt\u002Fsupabase-function-security",{"title":5,"description":1322},"nuxt-fullstack","Nuxt 4 全棧實戰筆記","blog\u002Fnuxt\u002Fsupabase-function-security\u002Findex",[1320,1333,1334,1335],"Supabase","PostgreSQL","RLS","uqe-9dNrf2alY4b380l2VI4olPE9WcSEhegPvXFUDOY",1780512499430]