ข้าม​ไป​ยัง​เนื้อหา

ยอด​รวม​ค้าง​อยู่​ที่​สาม​ทุก​สถานะ แต่​สาม​แถว​นั้น​เปลี่ยน​เจ้าของ​ไป​แล้ว

ห้า​บท​ที่​ผ่าน​มา​วัด​ทุก​อย่าง​ด้วย​จำนวน​แถว และ​หน่วย​นั้น​ทำงาน​ได้​ดี​ทุก​ครั้ง เพราะ​การ​รั่ว​ใน​คอร์ส​นี้​ไม่มี​รหัส​ข้อ​ผิด​พลาด​ให้​ดู มี​แต่​แถว​ที่​ผู้​อ่าน​ได้​กลับ​ไป

บท​นี้​วัด​ทรง​ที่​หน่วย​นั้น​มอง​ไม่​เห็น app role ตั้ง app.tenant เป็น clinic-a แล้ว​นับ​แถว​ได้ 3 ทั้ง​ก่อน​โจมตี ระหว่าง​โจมตี และ​หลัง​ปิด​ช่อง

สาม​สถานะ​ให้​เลข​เดียวกัน​หมด แต่​ใน​สถานะ​กลาง แถว​ทั้ง​สาม​ที่​ได้​กลับ​ไป ไม่มี​แถว​ไหน​เป็น​ของ clinic-a เลย​สัก​แถว

📦 clinic — ซี​รีส์​เดิม แต่​หกบท​แรก​เป็น SQL ล้วน

คอร์ส​นี้​อยู่​ใน​ซี​รีส์​คลินิก​เดียว​กับ คอร์ส​คัด​กรอง​ผู้​เรียก และ คอร์ส CORS กับ CSRF แต่​หกบท​แรก​ไม่มี​แอป​มา​เกี่ยวข้อง มี​แต่​ตาราง policy และ role

ตัวเลข​ทุก​ตัว​ใน​บท​นี้​คุณ​รัน​เอง​ได้ สั่ง scripts/clinic-rls/up.sh ซึ่ง​ยก container ชื่อ kaen-rls-pg ขึ้น​ที่ port 55438 แล้ว​สั่ง python3 scripts/clinic-rls/spike.py เก็บ​กวาด​ด้วย docker rm -f kaen-rls-pg

policy ไม่​ได้​เทียบ​ค่า​เอง มัน​เรียก​ฟังก์ชัน และ​ฟังก์ชัน​นั้น​เรียก​ฟังก์ชัน​อีก​ตัว

ทรง​นี้​ใช้​ตาราง t_hijack ซึ่ง​หว่าน​ด้วย​เมล็ด​ก้อน​เดียว​กับ​บท​ที่ 1 คือ​หก​แถว สาม​แถว​ต่อ​ผู้​เช่า​หนึ่ง​ราย ต่าง​จาก​บท​ก่อน​หน้า​ตรง​ที่​นิพจน์​ของ policy ไม่​ได้​เทียบ tenant_id กับ​ค่า​ตั้ง​ตรงๆ แต่​ส่ง​ต่อ​ให้​ฟังก์ชัน

ทรง​ที่ 13 · สั่ง​โดย role ที่​เป็น​เจ้าของ​ตาราง
create table t_hijack (
id bigserial primary key,
tenant_id text not null,
patient text not null,
archived boolean not null default false);
insert into t_hijack (tenant_id, patient, archived) values
('clinic-a','Ann',false),('clinic-a','Ben',false),('clinic-a','Cid',true),
('clinic-b','Dan',false),('clinic-b','Eve',false),('clinic-b','Fay',true);
grant select, insert, update, delete on t_hijack to kaen_rls_app, kaen_rls_bypass;
grant usage, select on sequence t_hijack_id_seq to kaen_rls_app, kaen_rls_bypass;
alter table t_hijack enable row level security;
create function public.guard() returns text language sql
as $$ select current_setting('app.tenant', true) $$;
create function public.tenant_ok(t text) returns boolean language plpgsql
as $$ begin return t = guard(); end $$;
create policy p on t_hijack for all using (public.tenant_ok(tenant_id));

ห่วง​โซ่​มี​สาม​ข้อ​ต่อ และ​ข้อ​ต่อ​ที่​สาม​คือ​ที่​ที่​บท​นี้​ทำงาน

  • policy เรียก public.tenant_ok ซึ่งระบุ schema ไว้​ใน​นิพจน์​แล้ว
  • tenant_ok เป็น plpgsql และ​ข้าง​ใน​มัน​เรียก guard() แบบไม่​ระบุ schema
  • guard() อ่าน​ค่า​ตั้ง​ของ session ด้วย current_settingcurrent_setting()ฟังก์ชัน​ที่​อ่าน​ค่า​ของ GUC · ใช้ได้​สอง​รูป · ทรง​ที่​วัด​คือ session เดียว​ที่​อ่าน​ตาราง t_guc · รูป​ที่​ไม่​ใส่​อาร์กิวเมนต์​ที่​สอง ยก​รหัส 42704 (csNoArgNeverSet) เมื่อ​ไม่มี​ใคร​เคย​ตั้ง​ค่า​นั้น​ใน session นี้ ส่วน​หลัง RESET รูป​เดียวกัน​ได้ 00000 (csNoArgAfterReset) และ​ค่าที่​ได้​เป็น​สตริง​ยาว 0 (csTrueAfterResetLen) แล้ว​คืน​ค่า​นั้น​ออก​มา

ชื่อ​ที่​ไม่​ระบุ schema ต้อง​ถูก​ค้นหา และ​ตัว​ที่​กำหนด​ลำดับ​การ​ค้น​คือ search_pathsearch_pathลำดับ schema ที่ PostgreSQL ใช้​ค้น​ชื่อ​ซึ่ง​ไม่​ได้​ระบุ schema ไว้ · ฟังก์ชัน​ของ policy ที่​ไม่​ได้​ตรึง​ค่า​นี้ ทำให้​แถว​ของ​ตาราง t_hijack ที่​ผู้​อ่าน​เห็น ย้าย​จาก​ผู้​เช่า a ไป​เป็น​ของ​ผู้​เช่า b · ยอด​ของ​ผู้​เช่า b ขยับ​จาก 0 เป็น 3 แล้วก​ลับ​เป็น 0 หลัง​ตรึง​ค่า​นี้​ลง​บน​ฟังก์ชัน ขณะ​ที่​ยอด​รวม​ค้าง​อยู่​ที่ 3 ทั้ง​สาม​สถานะ ของ session ที่​กำลัง​เรียก ซึ่ง​ใน​ทรง​นี้​คือ session ของ​ผู้​อ่าน ไม่ใช่​ของ​เจ้าของ​ตาราง

อ่าน​ค่า​นี้​จาก pg_proc ก่อน​แตะ​อะไร​ทั้ง​สิ้น เพราะ​ทั้ง​บท​ตั้ง​อยู่​บน​คำ​ตอบ​ของ​มัน

สั่ง​โดย​เจ้าของ​ตาราง · ก่อน​การ​วัด​ทุก​ทรง​ใน​บท​นี้
select 'KAEN|hijackFnHasSearchPath|'||(proconfig is not null)
from pg_proc where oid = 'public.tenant_ok(text)'::regprocedure;

hijackFnHasSearchPath = false

ค่า​นี้​อ่าน​ออก​มา​จาก pg_proc จริง ไม่ใช่​สิ่ง​ที่​บท​นี้​เดา​จาก DDL ข้าง​บน false แปล​ว่า​ฟังก์ชัน​ตัว​นี้​ไม่มี​ค่า​ตั้ง​ของ​ตัว​เอง​ติด​มา​สัก​ตัว มัน​จึง​ใช้ search_path ของ​ผู้​เรียก​ตอน​ที่​ถูก​เรียก

ฟังก์ชัน​ที่​เขียน​แบบ​นี้​อ่าน​แล้ว​ไม่มี​อะไร​น่า​สงสัย มัน​สั้น ทำงาน​อย่าง​เดียว และ​ไม่มี​คำ​ว่า search_path อยู่​ใน​ตัว​มัน​เลย

ทรง​ตั้ง​ต้น​ที่​ทุก​ตัวเลข​ข้าง​หลัง​จะ​ถูก​เทียบ​กลับ​มา​ที่​นี่

ผู้​อ่าน​คือ app role ซึ่ง​ไม่ใช่​เจ้าของ​ตาราง และ​ไม่มี​ธง BYPASSRLS
set app.tenant = 'clinic-a';
select 'KAEN|hijackBaseTotal|'||count(*) from t_hijack where true;
select 'KAEN|hijackBaseRowsB|'||count(*) from t_hijack where tenant_id='clinic-b';
คีย์ค่าที่​วัด​ได้
hijackBaseTotal3
hijackBaseRowsB0

สอง​แถว​นี้​คือ​รูป​ที่​ควร​จะ​เป็น ผู้​อ่าน​ที่​ตั้ง​ตัว​เอง​เป็น clinic-a เห็น​สาม​แถว และ​ใน​สาม​แถว​นั้น​ไม่มี​แถว​ของ clinic-b ปน​มา​สัก​แถว

เมล็ด​ของ​ตาราง​มี​ผู้​เช่า​สอง​ราย​เท่านั้น สาม​แถว​ที่​นับ​ได้​จึง​เป็น​ของ clinic-a ทั้ง​สาม​แถว ข้อ​นี้​เป็นการ​อ่าน​เลข​สอง​ตัว​ข้าง​บน​ประกอบ​กัน ไม่ใช่​คีย์​ตัว​ที่​สาม

ผู้​โจมตี​สร้าง​ฟังก์ชัน​ชื่อ​เดียวกัน​ใน pg_temp แล้ว​วาง​มัน​ไว้​หน้า search_path

pg_temp คือ schema ชั่วคราว​ของ session ตัว​เอง app role มี​สิทธิ์​สร้าง​ของ​ใน​นั้น ตั้งแต่​ต้น​โดย​ไม่​ต้อง​ขอ​ใคร ทาง​นี้​จึง​เป็น​ทาง​แรก​ที่​ใคร​ก็ตาม​จะ​ลอง

ความ​พยายาม​ที่​หนึ่ง · session เดียว · สั่ง​โดย app role
set app.tenant = 'clinic-a';
create temp table t_anchor (x int);
create function pg_temp.guard() returns text language sql as $$ select 'clinic-b'::text $$;
set search_path = pg_temp, public;
select 'KAEN|hijackTempQualified|'||pg_temp.guard();
select 'KAEN|hijackTempUnqualified|'||guard();
select 'KAEN|hijackTempTotal|'||count(*) from t_hijack where true;
select 'KAEN|hijackTempRowsB|'||count(*) from t_hijack where tenant_id='clinic-b';
คีย์ค่าที่​วัด​ได้
hijackTempQualifiedclinic-b
hijackTempUnqualifiedclinic-a
hijackTempTotal3
hijackTempRowsB0

สอง​แถว​บน​คือ​หัวใจ​ของ​ทรง​นี้ เรียก​ด้วย​ชื่อ​ที่​ระบุ schema ได้ clinic-b ซึ่ง​ยืนยัน​ว่า​ฟังก์ชัน​ของ​ผู้​โจมตี​มี​อยู่​จริง​และ​คืน​ค่าที่​ตั้งใจ​ไว้​จริง

แต่​ชื่อ guard() ที่​ไม่​ระบุ schema ยัง​ได้ clinic-a ทั้ง​ที่ pg_temp อยู่​หน้า​สุด​ของ search_path แล้ว สอง​แถว​ล่าง​จึง​ไม่​ขยับ​จาก​ทรง​ตั้ง​ต้น​เลย

เอกสาร​ของ PostgreSQL 18 อธิบาย​กฎ​ข้อ​นี้​ไว้​เอง คือ schema ชั่วคราว​ถูก​ค้น​สำหรับ​ชื่อ​ของ relation และ​ชื่อ​ของ data type แต่​ไม่​ถูก​ค้น​สำหรับ​ชื่อ​ฟังก์ชัน​และ​ชื่อ operator ข้อ​นี้​เป็น​ข้อ​อ้าง​เรื่อง​กลไก จึง​อ้าง​เอกสาร ส่วน​สี่​ค่า​ข้าง​บน​อ้าง probe

ทาง​เดีย​วกันเป๊ะ เปลี่ยน​แค่ schema ที่​ฟังก์ชัน​ไป​นั่ง​อยู่ แล้ว​ผลก​ลับ​ด้าน

schema ที่​ทรง​นี้​ใช้​ชื่อ app_own และ DBA เป็น​คน​สร้าง​ให้ ไม่ใช่ app role สร้าง​เอง บรรทัด​นี้​อยู่​ใน​ขั้น​ตั้ง​ฐาน​ข้อมูล​ของ probe และ​ผู้​สั่ง​คือ postgres

สั่ง​โดย DBA ตอน​ตั้ง​ฐาน​ข้อมูล · ไม่​ได้​สั่ง​โดย app role
create schema app_own authorization kaen_rls_app;
ความ​พยายาม​ที่​สอง · session เดียว · สั่ง​โดย app role
set app.tenant = 'clinic-a';
create function app_own.guard() returns text language sql as $$ select 'clinic-b'::text $$;
set search_path = app_own, public;
select 'KAEN|hijackOwnUnqualified|'||guard();
select 'KAEN|hijackOwnTotal|'||count(*) from t_hijack where true;
select 'KAEN|hijackOwnRowsA|'||count(*) from t_hijack where tenant_id='clinic-a';
select 'KAEN|hijackOwnRowsB|'||count(*) from t_hijack where tenant_id='clinic-b';
คีย์ค่าที่​วัด​ได้
hijackOwnUnqualifiedclinic-b
hijackOwnTotal3
hijackOwnRowsA0
hijackOwnRowsB3

แถว​บน​สุด​คือ​ความ​ต่าง​ทั้งหมด​ระหว่าง​สอง​ความ​พยายาม ชื่อ​ที่​ไม่​ระบุ schema ตัว​เดียวกัน เดิน​ไป​ถึง​ฟังก์ชัน​ของ​ผู้​โจมตี​แล้ว และ​คืน clinic-b ออก​มา​ให้ policy ใช้​ตัดสิน

hijackOwnTotal = 3 เท่ากับ hijackBaseTotal = 3 เป๊ะ แต่ hijackOwnRowsA = 0 และ hijackOwnRowsB = 3 คือ​ทุก​แถว​ที่​ผู้​อ่าน​ได้​กลับ​ไป เป็น​ของ​ผู้​เช่า​ที่​ไม่ใช่​ตัว​เอง

app.tenant ยัง​เป็น clinic-a ตลอด​ทรง​นี้ ไม่มี​ใคร​แตะ policy ไม่มี​ใคร​แตะ​ตาราง และ​ไม่มี​คำ​สั่ง​ไหน​ล้ม​เหลว สิ่ง​เดียว​ที่​เปลี่ยน​คือ​ลำดับ​การ​ค้น​ชื่อ

ทั้ง​สอง​ข้อ​ต้อง​เป็น​จริง​พร้อม​กัน และ​ข้อ​ที่​สอง​ไม่​ได้​มา​เอง

ข้อ​แรก ทาง pg_temp ใช้​ไม่​ได้ hijackTempUnqualified = clinic-a ปิด​ทาง​นั้น​ไป​แล้ว ผู้​โจมตี​ที่​มี​แค่ schema ชั่วคราว​ของ​ตัว​เอง ไป​ไม่​ถึง​ฟังก์ชัน​ที่ policy เรียก

ข้อ​ที่​สอง ต้อง​มี schema ที่ app role เป็น​เจ้าของ ใน​ทรง​นี้​คือ app_own ซึ่ง DBA สั่ง create schema ... authorization ให้ ตาม​บรรทัด​ที่​ยก​มา​ข้าง​บน

สอง​ข้อ​นี้​ต้อง​อ่าน​คู่​กัน ไม่ใช่​อ่าน​แยก

ถ้า​อ่าน​เฉพาะ​ตัวเลข​ของ​ความ​พยายาม​ที่​สอง จะ​ได้​ข้อ​สรุป​ว่า search_path ที่​ไม่​ถูก​ตรึง​เปิด​ช่อง​ให้​ทุก role ที่​ต่อ​ฐาน​ข้อมูล​ได้ ซึ่ง​ไม่ใช่​สิ่ง​ที่​ทรง​นี้​วัด

ถ้า​อ่าน​เฉพาะ​ตัวเลข​ของ​ความ​พยายาม​ที่​หนึ่ง จะ​ได้​ข้อ​สรุป​ตรง​ข้าม​ว่า​ช่อง​นี้​ปิด​อยู่​แล้ว ซึ่ง​ก็​ไม่ใช่​สิ่ง​ที่​ทรง​นี้​วัด​เช่น​กัน คำ​ตอบ​อยู่​ที่​การ​อ่าน​สอง​ชุด​คู่​กัน

ผู้​โจมตี​ยัง set search_path ชุด​เดิม และ​ฟังก์ชัน​ของ​เขา​ยัง​อยู่​ที่​เดิม

สอง​ส่วน · บรรทัด​แรก​สั่ง​โดย​เจ้าของ​ฟังก์ชัน · สี่​บรรทัด​ที่​เหลือ​สั่ง​โดย app role
alter function public.tenant_ok(text) set search_path = pg_catalog, public;
set app.tenant = 'clinic-a';
set search_path = app_own, public;
select 'KAEN|hijackPinnedTotal|'||count(*) from t_hijack where true;
select 'KAEN|hijackPinnedRowsB|'||count(*) from t_hijack where tenant_id='clinic-b';
คีย์ค่าที่​วัด​ได้
hijackPinnedTotal3
hijackPinnedRowsB0

app_own.guard ยัง​มี​อยู่ และ search_path ของ​ผู้​อ่าน​ยัง​ชี้​ไป​ที่ app_own ก่อน แต่​ค่าที่​วัด​ได้​กลับ​ไป​เท่ากับ​ทรง​ตั้ง​ต้น​ทั้ง​สอง​คีย์

คำ​สั่ง​ที่​ปิด​ช่อง​นี้​เป็น ALTER FUNCTION และ​มัน​ผูก​ค่า​ตั้ง​ไว้​กับ​ตัว​ฟังก์ชัน ฟังก์ชัน​จึง​ไม่​ได้​ใช้ search_path ของ​ผู้​เรียก​อีก​ต่อ​ไป เอกสาร​ของ PostgreSQL 18 แนะนำ​วิธี​นี้​ไว้​เอง​ใน​หัวข้อ​การ​เขียน​ฟังก์ชัน​อย่าง​ปลอดภัย

ส่วน​ชื่อ public.tenant_ok ที่​นิพจน์​ของ policy เรียก​นั้น ระบุ schema ไว้​อยู่​แล้ว และ​เอกสาร​เรื่อง dependency tracking อธิบาย​ว่า PostgreSQL บันทึก​การ​อ้าง​ถึง object ที่​ปรากฏ​ใน​นิพจน์​ที่​เก็บ​ไว้ เป็นการ​อ้าง​ถึง​ตัว object เอง ข้อ​นี้​อ้าง​เอกสาร ไม่​ได้​อ้าง probe

ยอด​รวม​เป็น 3 ทั้ง​สี่​แถว​ของ​ตาราง และ​มี​แถว​เดียว​ที่​ผิด

flowchart TB
  Q["app role ตั้ง app.tenant เป็น clinic-a แล้วนับแถวใน t_hijack"] --> P["policy p เรียก public.tenant_ok ซึ่งระบุ schema ไว้ในนิพจน์"]
  P --> F["ข้างใน tenant_ok เรียกชื่อ guard แบบไม่ระบุ schema"]
  F --> S{"ฟังก์ชัน tenant_ok ตรึง search_path ไว้หรือยัง"}
  S -->|"ตรึงแล้วด้วย alter function"| R["public.guard ถูกเรียก · 3 แถว · ของ b เป็น 0 · hijackPinnedTotal และ hijackPinnedRowsB"]
  S -->|"ยัง · hijackFnHasSearchPath เป็น false"| U{"search_path ของผู้อ่านชี้ไปที่ schema ใดก่อน"}
  U -->|"pg_temp แล้วค่อย public"| T["ชื่อฟังก์ชันข้าม pg_temp · public.guard ถูกเรียก · 3 แถว · ของ b เป็น 0"]
  U -->|"app_own แล้วค่อย public"| A["app_own.guard ถูกเรียก · 3 แถว · ของ a เป็น 0 และของ b เป็น 3"]

คำ​บรรยาย​ภาพ: ทาง​เดิน​ของ​ชื่อ guard ที่​ไม่​ระบุ schema เป็น​ตัว​ตัดสิน​ว่า​ผู้​อ่าน​ได้​แถว​ของ​ใคร ปลาย​ทาง​ทั้ง​สาม​ใบ​เขียน​ว่า​สาม​แถว​เท่า​กัน​หมด และ​มี​ใบ​เดียว​ที่​สาม​แถว​นั้น​เป็น​ของ​ผู้​เช่า​คน​อื่น

สถานะยอด​รวมแถว​ของ clinic-b
ก่อน​โจมตี3 · hijackBaseTotal0 · hijackBaseRowsB
search_path = pg_temp, public3 · hijackTempTotal0 · hijackTempRowsB
search_path = app_own, public3 · hijackOwnTotal3 · hijackOwnRowsB
ตรึง search_path ลง​บน​ฟังก์ชัน​แล้ว3 · hijackPinnedTotal0 · hijackPinnedRowsB

ด่าน​ที่​เฝ้า​แต่​ยอด​รวม ผ่าน​การ​โจมตี​นี้​ไป​อย่าง​สะอาด

หัวข้อ​ที่​มีชื่อ​ว่า “ด่าน​ที่​เฝ้า​แต่​ยอด​รวม ผ่าน​การ​โจมตี​นี้​ไป​อย่าง​สะอาด”

หน่วย​ที่​คอร์ส​นี้​ใช้​มา​ห้า​บท มี​จุดบอด​หนึ่ง​จุด และ​จุด​นั้น​อยู่​ใน​ตาราง​ข้าง​บน

สมมติ​มี​ด่าน​ที่​ยิง​คำขอ​เข้า​มา​แล้ว​นับ​แถว​ที่​ได้​กลับ​ไป ถ้า​เลข​เป็น​สาม​ก็​ถือว่า​ผ่าน ด่าน​แบบ​นั้น​ให้​ผล​เขียว​ทั้ง​สี่​แถว​ของ​ตาราง​ข้าง​บน รวม​ทั้ง​แถว​ที่​รั่ว

สิ่ง​ที่​ด่าน​นั้น​ไม่​ได้​ตรวจ​คือ แถว​ที่​ได้​กลับ​ไป​เป็น​ของ​ใคร ซึ่ง​เป็น​คีย์​คนละ​ตัว คือ hijackOwnRowsA และ hijackOwnRowsB ไม่ใช่ hijackOwnTotal

ข้อ​อ้าง​ของ​บท​นี้ ผูก​กับ​สิ่ง​ที่ probe วัด​เท่านั้น

probe วัด​การ​ค้น​ชื่อ ฟังก์ชัน ทรง​เดียว กฎ​ของ​ชื่อ ตาราง ไม่​เหมือน​กัน และ​คอร์ส​นี้​ไม่​ได้​วัด​มัน​สัก​ทรง ข้อ​อ้าง​ของ​บท​นี้​จึง​ไม่​ครอบ​ชื่อ​ตาราง

ผล​ทั้งหมด​วัด​บน PostgreSQL 18.6 บน aarch64-unknown-linux-musl ใน container ของ​คอร์ส​นี้​เอง major อื่น​เป็น​คนละ​การ​วัด

spike.py รัน​ชุด​ข้อ​อ้าง​ทั้ง​ชุด​สาม​รอบ​ต่อ​การ​เรียก​หนึ่ง​ครั้ง รอบ​ที่​รัน​เมื่อ 2026-09-06 ได้ 52 ข้อ​อ้าง​ตรง​ทั้งหมด 98 ค่าที่​วัด​ได้ และ 0 ค่าที่​ขยับ​ระหว่าง​รอบ

ค่า​ตั้ง​ที่ policy อ่าน ถูก​อ่าน​ผิด​ตัว​ได้​แล้ว คำถาม​ถัด​ไป​คือ​มัน​อยู่​ได้​นาน​แค่​ไหน

🔁 บท​ที่ 5 กับ​บท​นี้​เข้าหา​ปัญหา​เดียวกัน​คนละ​ด้าน บท​ที่ 5 ถาม​ว่า​ใคร​เป็น​คน​เรียก​นิพจน์ บท​นี้​ถาม​ว่า​นิพจน์​นั้น​อ่าน​ค่า​มา​จาก​ที่ไหน

ทุก​ทรง​ใน​บท​นี้​อยู่​ใน session เดียว และ​ตั้ง app.tenant ใหม่​ทุก​ครั้ง​ที่​วัด ส่วน 🔁 บท​ที่ 2 วัด​ไว้​แล้ว​ว่า​ค่า​นั้น​มีอายุ​ของ​มัน​เอง และ​การ​ล้าง​ค่า​ไม่​ได้​คืน​สถานะ​ที่​ไม่​เคย​ตั้ง

บท​ที่ 7 ย้าย​ทั้งหมด​นี้​ไป​อยู่​หลัง​แอป​ที่​ใช้ connection pool แล้ว​วัด​ว่า​คำขอ​ที่​สอง บน connection เดิม ได้​แถว​ของ​ใคร ที่​นั่น​ไม่มี​ใคร​สั่ง set search_path เลย​สัก​บรรทัด แต่​ค่าที่ policy อ่าน ยัง​เป็น​ค่าที่​คน​อื่น​ตั้ง​ไว้


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • PostgreSQL 18 — search_path (เข้า​ถึง 2026-09-06) — ที่มา​ของ​กฎ​ว่า schema ชั่วคราว​ถูก​ค้น​สำหรับ​ชื่อ​ของ relation และ​ชื่อ​ของ data type แต่​ไม่​ถูก​ค้น​สำหรับ​ชื่อ​ฟังก์ชัน​และ​ชื่อ operator

  • PostgreSQL 18 — CREATE FUNCTION (เข้า​ถึง 2026-09-06) — การ​ผูก​ค่า​ตั้ง​ไว้​กับ​ตัว​ฟังก์ชัน และ​หัวข้อ​การ​เขียน​ฟังก์ชัน​อย่าง​ปลอดภัย

  • PostgreSQL 18 — Dependency Tracking (เข้า​ถึง 2026-09-06) — การ​บันทึก​การ​อ้าง​ถึง object ที่​ปรากฏ​ใน​นิพจน์​ที่​เก็บ​ไว้

  • PostgreSQL 18 — ALTER FUNCTION (เข้า​ถึง 2026-09-06) — ความ​หมาย​ของ set search_path ที่​ผูก​ลง​บน​ฟังก์ชัน​ที่​มี​อยู่​แล้ว

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก​ทรง​ที่ 13 ของ scripts/clinic-rls/spike.py ซึ่ง​สร้าง​ฐาน​ข้อมูล กับ role ขึ้น​ใหม่​ทุกร​อบ แล้ว​รื้อ​ทิ้ง​เมื่อ​จบ · ต้อง​สั่ง scripts/clinic-rls/up.sh ก่อน ซึ่ง​ยก container ของ​คอร์ส​นี้​เอง​ขึ้น​ที่ port 55438

เช็กความเข้าใจ — บทที่ 6

ข้อ 1 / 3

app role ตั้ง `app.tenant` เป็น `clinic-a` แล้วสั่ง `set search_path = app_own, public` โดยที่ `app_own.guard()` คืน `clinic-b` ผู้อ่านนับแถวได้เท่าไร และเป็นแถวของใคร