Skip to content

6 · Scorecard + debt register (V1 depth)

บทสรุปของ 01 ถึง 05 สำหรับ recruitment-api (branch: prod) — severity catalog แบบเจาะลึกแต่ละ entry (exploit / impact / evidence / remediation) + health scorecard + modernization priority

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


Severity legend

Levelเกณฑ์Action
Criticalactive security/data-loss/availability riskต้องแก้ก่อน deploy ถัดไป / rotate ทันที
Highcorrectness/maintainability hazard, real impactวางแผนแก้ใน quarter นี้
Mediumfriction/hygiene, ยังไม่อันตรายระดับ activeแก้ตามโอกาส
Lowcleanup / docs / test depthbacklog

Severity: Critical

SEC-RECRUIT-01 · Production credential หลุดเข้า git — MSSQL, JWT, SMTP, reCAPTCHA, GCP SA key

ที่:

  • recruitment-api/.env (branch: prod)DB_* / DB_AI_* / VTRC_DB_* / WORKFORCE_DB_* passwords, SECRET_KEY, REFRESH_TOKEN_SECRET_KEY, SMTP_PASS, RECAPTCHA_SECRET_KEY, RC_WEB_API_KEY
  • recruitment-api/vtrc-gcp-sa-key.json (branch: prod) — GCP service account key ทั้งไฟล์
  • Device keys hardcode ใน recruitment-api/config/configuration.ts (branch: prod):75-104

Evidence: ไฟล์ .env มี NODE_ENV=prod และ credential จริง commit อยู่ใน working tree ของ branch prod — pattern เดียวกับ benefit-api และ vtrc-api/vtrc-gcp-sa-key.json (workspace canonical-truths). SECRET_KEY ถูกใช้ร่วมกับ VTRC JWT verify (ดู 04 Auth) จึงขยาย blast radius ข้าม service

Exploit:

bash
git clone <recruitment-api-repo>
git checkout prod
# อ่านชื่อตัวแปร (อย่า paste ค่าใน chat/ticket)
grep -E '^(DB_|SECRET_|SMTP_|RECAPTCHA_|RC_WEB)' .env | cut -d= -f1
# ถ้า network ถึง DB host → เชื่อม MSSQL ด้วย DB_USER/DB_PASSWORD ได้
# ถ้าได้ SECRET_KEY → forge JWT ที่ JwtStrategy ยอมรับ (รวม iss VTRC)

Impact:

  • Full read/write บน schema recruitment + recruitment_ai (และ connection ที่ตั้งไว้แม้ยังไม่ใช้ inject)
  • Forge candidate-portal JWT และ/หรือ VTRC-compatible token
  • ส่งอีเมลในนาม SMTP ขององค์กร
  • ใช้ GCP SA ตามสิทธิ์ใน JSON (deploy / GCR ตามที่ key อนุญาต)
  • Shared-secret SSO แตก → กระทบ service อื่นที่แชร์ SECRET_KEY เดียวกัน

Remediation:

  1. Rotate ทุก credential ที่เคยอยู่ใน .env + GCP key ทันที
  2. เพิ่ม .env และ *-sa-key.json เข้า .gitignore; ลบออกจาก git history (git filter-repo)
  3. ย้ายไป secret manager / k8s Secret / Bitbucket secured variables เท่านั้น
  4. Device keys ใน configuration.ts ย้ายออกจาก source
  5. ตรวจสอบว่า SECRET_KEY ใหม่ synchronize กับทุก service ที่ verify shared JWT จริง ๆ ก่อน cutover

SEC-RECRUIT-02 · SQL injection ผ่าน dynamic EXEC stored procedure string (connection ai)

ที่: recruitment-api/src/modules/candidate/candidate.repositories.ts (branch: prod):1324-1338

Evidence:

typescript
public async getMultipleByIdAI(query: GetCandidateCompareByAIDto) {
  const multipleId = query.multipleId?.includes(',')
    ? query.multipleId.split(',')
    : [query.multipleId];
  let param = '@Input_Job_ID = ' + query.jobId + ', ';
  for (let i = 0; i < 5; i++) {
    let id = '';
    if (multipleId[i]) {
      id = `'${multipleId[i]}'`;
    } else {
      id = null;
    }
    param = param.concat(
      '@Input_Candidate_ID' + (i + 1) + ' = ' + `${id}` + (i === 4 ? '' : ', '),
    );
  }
  const querySelect = `EXEC Module3_3_Get_Selected_Candidate_Per_Job ${param}`;
  const resultSP = await this.entityManagerAi.query(querySelect);
  // ...
}

query.jobId และสมาชิกของ query.multipleId มาจาก HTTP query/DTO โดยตรง — ต่อเข้า string โดยไม่ escape single quote และไม่ใช้ parameter binding ต่างจาก endpoint ส่วนใหญ่ของ repo ที่ใช้ TypeORM QueryBuilder แบบ parameterized

Exploit (แนวคิด — อย่ารันบน production โดยไม่ได้รับอนุญาต):

bash
# multipleId ที่ปิด quote แล้วฉีดคำสั่งต่อท้าย
# ตัวอย่างรูปแบบ: 1'); EXEC sp_something; --
curl -G "https://.../candidate/compare_ai" \
  --data-urlencode "jobId=1" \
  --data-urlencode "multipleId=1'); WAITFOR DELAY '0:0:5';--" \
  -H "Authorization: Bearer <valid-jwt>"

Impact: SQL injection บน database recruitment_ai (connection name ai) — อ่าน/แก้ข้อมูล AI compare, รันคำสั่งเพิ่มผ่าน EXEC, หรือ denial-of-service ด้วย delay/heavy query ตามสิทธิ์ DB user ของ DB_AI_*

Remediation:

  1. เปลี่ยนเป็น parameterized call เช่น entityManagerAi.query('EXEC Module3_3_Get_Selected_Candidate_Per_Job @Input_Job_ID=@0, @Input_Candidate_ID1=@1, ...', [jobId, id1, ...])
  2. Validate jobId / candidate ids เป็นตัวเลขหรือ UUID ตาม schema จริงก่อนเรียก SP
  3. Sweep repo ด้วย rg "entityManagerAi\\.query\\(\|\.query\(`EXEC"` เพื่อยืนยันว่าไม่มีจุดดิบอื่น

SEC-RECRUIT-03 · POST /blacklist/import_blacklist เป็น @Public() — import บัญชีดำโดยไม่ต้อง JWT

ที่: recruitment-api/src/modules/blacklist/blacklist.controller.ts (branch: prod):20-27

Evidence:

typescript
@Public()
@UseInterceptors(FileInterceptor('file'))
@ApiOperation({ summary: 'Import Excel File' })
@Post('import_blacklist')
async importBlacklist(@UploadedFile() file: Express.Multer.File) {
  const buffer = file?.buffer;
  const data = await this.service.importBlacklist(buffer);
  return { data };
}

Global guard ที่ main.ts:39 จะข้าม JWT ทั้งก้อนเมื่อมี @Public() — ไม่มี apiKey / role check บน route นี้

Exploit:

bash
curl -X POST "https://.../recruitment-api/api/v1/blacklist/import_blacklist" \
  -F "file=@malicious-or-bulk.xlsx"
# → ใครก็ตามที่เข้าถึง network path ได้สามารถเพิ่ม/อัปเดต blacklist ได้

Impact: ปลอมแปลงบัญชีดำผู้สมัคร → ผู้สมัครถูก reject โดยไม่ผ่านกระบวนการทีมสรรหา; หรือ flood ข้อมูลขยะเข้า tbBlacklist / cascade ไปสถานะ candidate (ดู blacklist service flow ในบท 2)

Remediation: ลบ @Public() ทันที; บังคับ JWT + checkRoleRc (ส่ง apiKey); จำกัด file type/size; ใส่ audit log ว่าใคร import


Severity: High

CORR-RECRUIT-01 · Race condition ตอนลด remainingManpower

ที่: recruitment-api/src/modules/contract/contract.service.ts (branch: prod):740-763

Evidence:

typescript
async updateRemainingManpower(...) {
  const jobReq = await this.entityManager.findOne(JobRequestHd, {
    where: { documentNo: jobReqDocumentNo, isDeleted: false },
    relations: ['jobReqPosition'],
  });
  // ... คำนวณ remaining ในแอปฯ ...
  await this.entityManager
    .createQueryBuilder(JobReqPosition, 'hd')
    .update()
    .set({ remainingManpower: remaining, updatedBy, updatedByName })
    .where('id = :id ', { id: jobReqPos.id })
    .execute();
}

อ่านค่า → คำนวณใน memory → update แยกคำสั่ง — ไม่ใช่ atomic SET remainingManpower = remainingManpower - 1 WHERE remainingManpower > 0

Exploit / repro:

text
T1, T2: confirm สัญญา 2 ฉบับพร้อมกัน สำหรับตำแหน่งที่ remainingManpower = 1
→ ทั้งคู่ read ได้ 1 → ทั้งคู่ write 0 (หรือค่าที่คำนวณจาก 1)
→ ได้ hired 2 คน ทั้งที่โควต้าเหลือ 1

Impact: overcommit อัตรากำลัง (จ้างเกินจำนวนที่อนุมัติ) — ข้อมูลธุรกิจผิด ไม่ใช่แค่ crash; ส่งผลต่อ workforce manpower sync และรายงาน HR

Remediation:

typescript
await this.entityManager.query(
  `UPDATE jobReqPosition
   SET remainingManpower = remainingManpower - 1, updatedBy = @0, updatedByName = @1
   WHERE id = @2 AND remainingManpower > 0`,
  [updatedBy, updatedByName, jobReqPos.id],
);
// ตรวจ rowsAffected === 1 ไม่งั้น throw business error

สำหรับ decline ให้ใช้ + 1 แบบ atomic เช่นกัน; พิจารณา transaction ครอบ confirm + candidate status + manpower


SEC-RECRUIT-04 · reCAPTCHA bypass เมื่อไม่ส่ง token

ที่: recruitment-api/src/modules/appProfile/appProfile.service.ts (branch: prod):1130-1141 (ดู 04 Auth)

Evidence:

typescript
async verifyCaptcha(token: string): Promise<boolean> {
  if (!token) return true; // ไม่มี token = ผ่าน
  // ...
}

Exploit: เรียก endpoint ที่พึ่ง verifyCaptcha โดยไม่ส่ง captcha field → ผ่านเสมอ

Impact: ลดประสิทธิภาพ anti-bot / anti-automation บน flow ที่เกี่ยวกับพนักงานสภากาชาด / profile — ไม่ใช่ full auth bypass แต่ทำให้ control ที่คิดว่ามีอยู่จริงกลายเป็น optional

Remediation: if (!token) return false ใน production; feature-flag เฉพาะ local/dev


SEC-RECRUIT-05 · x-refresh-token header bypass Passport validate

ที่: recruitment-api/libs/jwt/jwt-auth.guard.ts (branch: prod):30-32 (อธิบายใน 04)

Evidence: ถ้ามี header x-refresh-tokencanActivate คืน true โดยไม่ผ่าน JwtStrategy — ออกแบบมาเพื่อเปิดทาง POST /auth/refresh_token แต่เป็นกลไกกว้างระดับ guard

Impact: endpoint ที่ไม่ได้ตั้งใจให้ public อาจถูกเรียกได้ถ้า client ส่ง header นี้ (ขึ้นกับว่า controller/service ตรวจ token ใน body เองหรือไม่) — soft auth hole

Remediation: ย้าย bypass ไปที่ @Public() เฉพาะ AuthController.refresh_token หรือ custom decorator @RefreshRoute() แทน global header check; service ต้อง validate refresh token เสมอ


QUAL-RECRUIT-01 · TypeORM connection vtrc / workforce ประกาศแต่ไม่ inject

ที่: recruitment-api/src/app.module.ts (branch: prod):42-53, config/configuration.ts:45-74

Evidence: grep @InjectEntityManager('vtrc') / ('workforce') ทั้ง src/ = ไม่พบ — แต่ connection ยังเปิด pool ตอน boot (credentials จาก .env รวมใน SEC-RECRUIT-01)

Impact: เสีย connection pool / credential surface โดยไม่มีประโยชน์; คนอ่านโค้ดเข้าใจผิดว่า query ตรงไป DB เหล่านั้น (จริงใช้ HTTP workforceServer / vtrcServer)

Remediation: ลบ connection ออกจาก AppModule + config ถ้าไม่มี roadmap; หรือ comment + ticket ชัดเจนถ้าสำรองไว้


Severity: Medium

SEC-RECRUIT-06 · CORS origin: '*' + Swagger เปิดในทุก env + body 50mb

ที่: recruitment-api/src/main.ts (branch: prod):35-48

Evidence:

typescript
app.use(bodyParser.json({ limit: '50mb' }));
app.enableCors({ origin: '*', allowedHeaders: '*', methods: '...' });
SwaggerModule.setup(`${process.env.BASE_PATH}docs`, app, document);

Impact: เบราว์เซอร์ใดก็ยิง cross-origin ได้ (ถ้ามี XSS / malicious site); Swagger เป็น API map ให้ attacker; body 50mb เพิ่ม DoS surface เมื่อไม่มี rate limit ด้านหน้า

Remediation: allowlist origin ตาม env; ปิด Swagger เมื่อ NODE_ENV=prod หรือป้องกันด้วย basic auth/VPN; rate-limit ที่ gateway; ลด body limit สำหรับ route ที่ไม่ต้องรับ base64 ใหญ่


SEC-RECRUIT-07 · MSSQL encrypt: false บนทุก connection

ที่: recruitment-api/config/configuration.ts (branch: prod):22-25,37-42,55-58,...

Evidence:

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

Impact: TDS ไม่บังคับ TLS — credential/query (รวม PII ผู้สมัคร) อาจถูกดักบนเครือข่ายระหว่าง app กับ DB ถ้า path ไม่ trusted เต็มที่

Remediation: encrypt: true + จัดการ certificate ให้ถูกต้อง; อย่าใช้ trustServerCertificate: true ใน production นานเกินช่วง migrate


OBS-RECRUIT-01 · console.log ปนกับ structured logger (~71 จุด)

ที่: กระจายใน src/modules/* และ libs/utils/* — ตัวอย่าง:

  • jobList.service.ts:99 — log ทั้ง token / refreshToken ตอน validate OTP
  • candidate.service.ts, appProfile.service.ts, email services

Evidence: นับจาก src+libs ประมาณ 71 จุด console.log ขณะมี I3GatewayLogger เป็นมาตรฐาน

Impact: PII / JWT หลุดเข้า stdout ของ container logs; ยากต่อการ filter ตาม log level

Remediation: eslint rule ห้าม console.log; แทนที่ด้วย logger + redact authorization, email, identity fields; ลบจุดที่ log token โดยด่วน


QUAL-RECRUIT-02 · VTRC_COMMON_SERVER ชี้ host ชื่อ -dev ใน production .env

ที่: recruitment-api/.env (branch: prod) (บรรทัดที่ตั้ง VTRC_COMMON_SERVER, มีคำว่า -dev ใน hostname) ขณะ NODE_ENV=prod

Evidence: ดู 04 — runtime ยัง ไม่พบ การอ่าน commonServer ใน src/ รอบนี้ (env เตรียมไว้แต่ยังไม่ wiring) — ความเสี่ยงตอนนี้ต่ำกว่า "เรียกผิด env จริง" แต่เป็น config smell และระเบิดเมื่อมีคนเริ่มใช้

Impact: ถ้าอนาคตมีโค้ดเรียก config.get('commonServer') จะยิงไป dev โดยไม่ตั้งใจ

Remediation: ยืนยันกับ infra; แก้ค่าใน secret store; เพิ่ม boot assert ห้าม hostname มี -dev เมื่อ NODE_ENV=prod


QUAL-RECRUIT-03 · Business rule ผูก client string โดยไม่มี enum

ที่: recruitment-api/src/modules/candidate/candidate.service.ts (branch: prod):456,491-504 — comment //web / //bo,ess

Impact: typo ("bo " / "ESS") ไม่ถูกจับตอน compile → สาขา logic ผิดเงียบ ๆ

Remediation: enum CandidateClient { Web = 'web', Bo = 'bo', Ess = 'ess', RcWeb = 'rc-web' } + class-validator @IsEnum


QUAL-RECRUIT-05 · Service ยาวเกิน (candidate 3,037 / announcement 2,645 / contract 1,895)

ที่: ดูตาราง module ใน 01

Impact: review ยาก, regression สูง, ทดสอบบาง path ไม่ทั่ว

Remediation: แยก PDF/Excel/email/AI compare เป็น facade services; ไม่ต้อง rewrite ทั้งก้อนใน PR เดียว


OBS-RECRUIT-03 · TypeORM logging: ['error', 'query'] ทุก connection ทุก env

ที่: configuration.ts:20,36,54,...

Impact: SQL + พารามิเตอร์ (อาจมีอีเมล/เลขบัตรใน WHERE) เข้า log production

Remediation: prod ใช้ ['error'] เท่านั้น; query log เฉพาะ dev/uat


Severity: Low

QUAL-RECRUIT-04 · ejs template สำรอง (.bak.ejs, _old.ejs) ใน repo

ที่: recruitment-api/libs/pdfTemplate/announcement/qualified-exam_old.ejs, *.bak.ejs

Impact: สับสนว่า template ไหน active; เสี่ยงแก้ไฟล์ผิด

Remediation: ลบออกจาก tree เหลือใน git history


OBS-RECRUIT-02 · Test coverage กระจุก — ความลึก assertion ยัง UNVERIFIED

ที่: พบ .spec.ts ~24 ไฟล์ครอบคลุมหลายโมดูล แต่ยังไม่ audit ความลึกของ assertion ในรอบนี้

Impact: ได้ false confidence จาก "มี spec"

Remediation: วัด coverage จริง; บังคับทดสอบ SEC-RECRUIT-02 (parameterized) และ CORR-RECRUIT-01 (concurrent)


QUAL-RECRUIT-06 · CENTRALIZE_SERVER / VTRC_COMMON_SERVER ใน config แต่ไม่ถูกเรียก

ที่: configuration.ts:6,8 — ไม่พบ config.get('centralizeServer'|'commonServer') ใน runtime path (04)

Impact: dead config; ทำให้คิดว่ามี integration ที่ไม่มี

Remediation: ลบหรือ document ว่า reserved; อย่าใส่ credential ของระบบที่ยังไม่ใช้


QUAL-RECRUIT-07 · Package name rest-structure ไม่ตรงชื่อ repo

ที่: recruitment-api/package.json (branch: prod):2

Impact: สับสนใน monorepo / CI artifact ชื่อ

Remediation: rename ใน major version หรือใส่ description ชัดเจน


Finding index (quick table)

IDSevหัวข้อสั้น
SEC-RECRUIT-01Critical.env + GCP SA ใน git
SEC-RECRUIT-02CriticalSQL injection EXEC AI compare
SEC-RECRUIT-03Critical@Public() import blacklist
CORR-RECRUIT-01Highrace remainingManpower
SEC-RECRUIT-04HighreCAPTCHA bypass ถ้าไม่มี token
SEC-RECRUIT-05Highx-refresh-token guard bypass
QUAL-RECRUIT-01Highunused DB connections
SEC-RECRUIT-06MediumCORS * / Swagger / 50mb body
SEC-RECRUIT-07Mediumencrypt: false
OBS-RECRUIT-01Mediumconsole.log + token leak
QUAL-RECRUIT-02Medium-dev URL ใน prod env
QUAL-RECRUIT-03Mediumclient string ไม่มี enum
QUAL-RECRUIT-05Mediumgod-services
OBS-RECRUIT-03Mediumquery logging ใน prod
QUAL-RECRUIT-04Low.bak.ejs
OBS-RECRUIT-02Lowtest depth UNVERIFIED
QUAL-RECRUIT-06Lowdead env wiring
QUAL-RECRUIT-07Lowpackage name mismatch

recruitment-api Health Scorecard

  • แดง — มีปัญหาจริง ควรแก้ก่อนขยายฟีเจอร์
  • เหลือง — ใช้ได้ แต่มีจุดอ่อน ต้องระวังตอนแก้
  • เขียว — ผ่านเกณฑ์สำหรับมิตินี้
Dimensionสถานะหมายเหตุ
Auth pipelineเหลืองJWT + authData revoke ได้ แต่ shared secret + refresh header + public blacklist
Persistence & transactionเหลืองQueryBuilder ส่วนใหญ่ดี; ยกเว้น EXEC string + manpower race
Secrets managementแดง.env + GCP key ใน git — Critical
External integration resilienceเหลืองaxios ไป workforce/vtrc ไม่มี retry/timeout ชัด; scope จำกัดกว่า benefit-api
Code duplicationเขียวไม่พบ copy-paste รุนแรงเท่า benefit payments SQL
Background jobsเขียวไม่มี cron — ดูบท 5; ความเสี่ยงย้ายไป sync path
Logging & observabilityเหลืองI3GatewayLogger มี แต่ console.log / query log / token log
LayeringเหลืองNest module มาตรฐาน แต่ service 2k–3k บรรทัด
Test coverageเหลืองมี spec หลายโมดูล; ความลึกยังไม่ยืนยัน
Config hygieneแดงprod secrets + unused connections + dead env + -dev host
Public surface controlเหลืองjobList public สมเหตุสมผล; blacklist import ไม่สมเหตุสมผล
Transport security (DB)เหลืองencrypt:false ทุก connection

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


Modernization — ลำดับ ROI

  1. Rotate + ลบ secrets จาก git history (SEC-RECRUIT-01) — เร่งด่วนเท่า benefit-api
  2. Parameterized EXEC AI compare (SEC-RECRUIT-02) — ปิดจุด injection ที่ยืนยันแล้วจุดเดียว
  3. ลบ @Public() จาก import_blacklist (SEC-RECRUIT-03)
  4. Atomic remainingManpower (CORR-RECRUIT-01)
  5. ปิด reCAPTCHA empty-token bypass + แคบ x-refresh-token (SEC-RECRUIT-04/05)
  6. ลบ unused TypeORM connections + แก้ encrypt/logging/CORS/Swagger (QUAL/SEC medium)
  7. Lint ห้าม console.log ที่ log token (OBS-RECRUIT-01)
  8. ยืนยัน VTRC_COMMON_SERVER กับ infra (QUAL-RECRUIT-02)
  9. แยก god-services + เพิ่ม concurrent tests (QUAL-RECRUIT-05 / OBS-RECRUIT-02)

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

ตรวจแล้ว (จากโค้ด + บท 01–05)ยังไม่ตรวจ / UNVERIFIED
ไม่มี @nestjs/schedule / @Cronpenetration test จริงบน UAT/prod
@Public() catalog ทั้งชุดว่า SECRET_KEY เท่ากับ benefit/vtrc ค่าไบต์ต่อไบต์หรือไม่ (ห้าม paste)
raw EXEC ที่ candidate.repositories.ts:1324-1338ว่า SP Module3_3_* มี permission อะไรบน SQL login
manpower read-modify-writeconcurrent load test จริง
.env + GCP key มีใน treeว่า history ถูก filter แล้วหรือยัง
unused vtrc/workforce connectionsroadmap ทีมจะใช้ connection เหล่านี้หรือไม่

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