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 |
|---|---|---|
| Critical | active security/data-loss/availability risk | ต้องแก้ก่อน deploy ถัดไป / rotate ทันที |
| High | correctness/maintainability hazard, real impact | วางแผนแก้ใน quarter นี้ |
| Medium | friction/hygiene, ยังไม่อันตรายระดับ active | แก้ตามโอกาส |
| Low | cleanup / docs / test depth | backlog |
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_KEYrecruitment-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:
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:
- Rotate ทุก credential ที่เคยอยู่ใน
.env+ GCP key ทันที - เพิ่ม
.envและ*-sa-key.jsonเข้า.gitignore; ลบออกจาก git history (git filter-repo) - ย้ายไป secret manager / k8s Secret / Bitbucket secured variables เท่านั้น
- Device keys ใน
configuration.tsย้ายออกจาก source - ตรวจสอบว่า
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:
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 โดยไม่ได้รับอนุญาต):
# 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:
- เปลี่ยนเป็น parameterized call เช่น
entityManagerAi.query('EXEC Module3_3_Get_Selected_Candidate_Per_Job @Input_Job_ID=@0, @Input_Candidate_ID1=@1, ...', [jobId, id1, ...]) - Validate
jobId/ candidate ids เป็นตัวเลขหรือ UUID ตาม schema จริงก่อนเรียก SP - 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:
@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:
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:
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:
T1, T2: confirm สัญญา 2 ฉบับพร้อมกัน สำหรับตำแหน่งที่ remainingManpower = 1
→ ทั้งคู่ read ได้ 1 → ทั้งคู่ write 0 (หรือค่าที่คำนวณจาก 1)
→ ได้ hired 2 คน ทั้งที่โควต้าเหลือ 1Impact: overcommit อัตรากำลัง (จ้างเกินจำนวนที่อนุมัติ) — ข้อมูลธุรกิจผิด ไม่ใช่แค่ crash; ส่งผลต่อ workforce manpower sync และรายงาน HR
Remediation:
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:
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-token → canActivate คืน 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:
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:
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 OTPcandidate.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)
| ID | Sev | หัวข้อสั้น |
|---|---|---|
| SEC-RECRUIT-01 | Critical | .env + GCP SA ใน git |
| SEC-RECRUIT-02 | Critical | SQL injection EXEC AI compare |
| SEC-RECRUIT-03 | Critical | @Public() import blacklist |
| CORR-RECRUIT-01 | High | race remainingManpower |
| SEC-RECRUIT-04 | High | reCAPTCHA bypass ถ้าไม่มี token |
| SEC-RECRUIT-05 | High | x-refresh-token guard bypass |
| QUAL-RECRUIT-01 | High | unused DB connections |
| SEC-RECRUIT-06 | Medium | CORS * / Swagger / 50mb body |
| SEC-RECRUIT-07 | Medium | encrypt: false |
| OBS-RECRUIT-01 | Medium | console.log + token leak |
| QUAL-RECRUIT-02 | Medium | -dev URL ใน prod env |
| QUAL-RECRUIT-03 | Medium | client string ไม่มี enum |
| QUAL-RECRUIT-05 | Medium | god-services |
| OBS-RECRUIT-03 | Medium | query logging ใน prod |
| QUAL-RECRUIT-04 | Low | .bak.ejs |
| OBS-RECRUIT-02 | Low | test depth UNVERIFIED |
| QUAL-RECRUIT-06 | Low | dead env wiring |
| QUAL-RECRUIT-07 | Low | package 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
- Rotate + ลบ secrets จาก git history (SEC-RECRUIT-01) — เร่งด่วนเท่า benefit-api
- Parameterized
EXECAI compare (SEC-RECRUIT-02) — ปิดจุด injection ที่ยืนยันแล้วจุดเดียว - ลบ
@Public()จากimport_blacklist(SEC-RECRUIT-03) - Atomic
remainingManpower(CORR-RECRUIT-01) - ปิด reCAPTCHA empty-token bypass + แคบ
x-refresh-token(SEC-RECRUIT-04/05) - ลบ unused TypeORM connections + แก้ encrypt/logging/CORS/Swagger (QUAL/SEC medium)
- Lint ห้าม console.log ที่ log token (OBS-RECRUIT-01)
- ยืนยัน VTRC_COMMON_SERVER กับ infra (QUAL-RECRUIT-02)
- แยก god-services + เพิ่ม concurrent tests (QUAL-RECRUIT-05 / OBS-RECRUIT-02)
สิ่งที่ตรวจแล้ว vs ยังไม่ตรวจ
| ตรวจแล้ว (จากโค้ด + บท 01–05) | ยังไม่ตรวจ / UNVERIFIED |
|---|---|
ไม่มี @nestjs/schedule / @Cron | penetration 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-write | concurrent load test จริง |
.env + GCP key มีใน tree | ว่า history ถูก filter แล้วหรือยัง |
unused vtrc/workforce connections | roadmap ทีมจะใช้ connection เหล่านี้หรือไม่ |
Cross-links
ไป กลับสู่ index หรือ ดูภาพรวม Volume 6