02 · Core pipeline — Public API, auth, SQL injection, consumers
vtrc-common ไม่มี request pipeline แบบ npm library (import class ข้าม process) — มันคือ NestJS HTTP service ที่ consumer เรียกผ่าน REST บทนี้อธิบาย bootstrap → auth → API ทั้ง 3 module → จุด SQL injection → แผนที่ผู้เรียกจริงจาก grep ทั้ง monorepo
อ้างอิง branch: prod
1. Bootstrap pipeline
ลำดับใน vtrc-common/src/main.ts (branch: prod):12-41:
NestFactory.create(AppModule, { logger: I3GatewayLogger })
│
├── setGlobalPrefix(`${BASE_PATH}api/v1`) → vtrc-common/api/v1
├── bodyParser.json / urlencoded limit 50mb → รองรับ base64 ไฟล์ใหญ่
├── useGlobalFilters(HttpExceptionFilter)
├── useGlobalGuards(JwtAuthGuard)
├── useGlobalPipes(ValidationPipe whitelist+forbidNonWhitelisted)
├── enableCors({ origin: '*', allowedHeaders: '*', ... })
├── SwaggerModule.setup(`${BASE_PATH}docs`, ...)
└── listen(port, '0.0.0.0')จุดที่ควรระวังตอนแก้:
| จุด | พฤติกรรม | หลักฐาน |
|---|---|---|
Body limit 50mb | อัปโหลด base64 หลายไฟล์ใน request เดียวได้ | main.ts:24-25 |
CORS origin: '*' | browser จาก origin ใดก็เรียกได้ | main.ts:29-33 |
ValidationPipe forbidNonWhitelisted | field นอก DTO → 400 | main.ts:28 |
Listen 0.0.0.0 | bind ทุก interface ใน container | main.ts:37 |
2. Auth pipeline — เกือบทุก route เป็น public
Guard
JwtAuthGuard.canActivate (vtrc-common/libs/jwt/jwt-auth.guard.ts (branch: prod):15-24):
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(isPublicKey, [
context.getHandler(),
context.getClass(),
]);
const request = context.switchToHttp().getRequest();
this.serviceName = request.url;
if (isPublic) {
return true;
}
return super.canActivate(context);
}@Public() = SetMetadata('isPublic', true) (:39-41)
Strategy
JwtStrategy (vtrc-common/libs/jwt/jwt.strategy.ts (branch: prod):10-22):
- ดึง token จาก
Authorization: Bearer ... ignoreExpiration: falsesecretOrKey: configService.get('jwtSecretKey')← envSECRET_KEYvalidate(payload)คืน payload ทั้งก้อนโดยไม่ตรวจuid/ role / issuer
สรุปสถานะ auth ต่อ endpoint
| Endpoint | @Public() | ผลจริง |
|---|---|---|
ทุก /address/* (6 route) | ใช่ | ไม่ต้อง JWT |
ทุก /bank/* (2 route) | ใช่ | ไม่ต้อง JWT |
POST /file/save, POST /file/delete | ใช่ | เขียน/soft-delete ได้โดยไม่ auth |
GET /file/:id, POST /file/getFileArray | ไม่ | ต้อง JWT — แต่ forge ได้ถ้ามี SECRET_KEY จาก .env ที่ commit |
ดู SEC-COMMON-01 / SEC-COMMON-03 ใน 04 Scorecard
3. Address module — query variants และ SQL injection
3.1 Safe paths (TypeORM / parameterized)
| Service method | Repository method | เทคนิค | หลักฐาน |
|---|---|---|---|
fetchListAllProvince | findAll | entityManager.find(MProvince, { isActive: true }) | address.repository.ts:17-23 |
fetchListAumphur | findAumphurByProvinceCode ผ่าน AddressUtil | TypeORM where + relations | repository.ts:25-39, address.util.ts:13-32 |
fetchListTumbol | findTumbolByProvinceCodeAndAumphurCode | TypeORM find + relations | repository.ts:44-59 |
fetchListTumbolV3 (/testTumbol2) | findQueryBuilderTumbolByProvinceCodeAndAumphurCode | QueryBuilder + .setParameter() | repository.ts:101-114 |
fetchListTumbolV4 (ไม่มี route) | findTumbolByProvinceCodeAndAumphurCodeV2 | QueryBuilder named params | repository.ts:65-74 |
ตัวอย่างที่ทำถูกแล้วในไฟล์เดียวกัน:
.where('p.province_code = :provCode')
.andWhere('a.aumphur_code = :amphCode')
.andWhere('a.is_active = 1')
.setParameter('provCode', provCode)
.setParameter('amphCode', amphCode)
.getRawMany();แหล่ง: vtrc-common/src/modules/address/address.repository.ts (branch: prod):109-114
3.2 Unsafe paths — SQL injection (SEC-COMMON-02)
สองเมธอดต่อ raw SQL ด้วย string interpolation ของ provCode / amphCode ที่มาจาก HTTP query (TumbolReqDto — @IsString() อย่างเดียว ไม่ sanitize):
A) findNativeTumbolByProvinceCodeAndAumphurCode — ถูกเรียกจาก GET /address/testTumbol → fetchListTumbolV2:
return await this.entityManager.query(`
SELECT t.tumbol_code as tumbolCode, ...
FROM m_aumphur a
INNER JOIN m_tumbol t ON a.id = t.aumphur_id
...
WHERE p.province_code = '${provCode}'
AND a.aumphur_code = '${amphCode}'
AND a.is_active = 1
`);แหล่ง: vtrc-common/src/modules/address/address.repository.ts (branch: prod):81-94
เรียกจาก: address.service.ts:60-70, address.controller.ts:32-36
B) findSubDistirctByProvCodeAndDistrictCode (สะกด District ผิดในชื่อเมธอด) — ถูกเรียกจาก GET /address/listSubDistrictByProvCodeAndDistrictCode:
WHERE p.province_code = '${provCode}'
AND a.aumphur_code = '${amphCode}'แหล่ง: vtrc-common/src/modules/address/address.repository.ts (branch: prod):117-130
เรียกจาก: address.service.ts:89-97, address.controller.ts:44-48
3.3 Attack surface
เพราะ route ทั้งคู่เป็น @Public():
- ผู้โจมตีไม่ต้องมี JWT
- ส่ง
provCodeหรือamphCodeเป็น payload SQL เช่น' OR 1=1-- - Query รันบน connection
main(DBvtrc_common) ด้วยบัญชีsaตาม.env
ผลกระทบ: อ่าน/แก้ข้อมูลใน vtrc_common ตามสิทธิ์ DB account (รวมตาราง file metadata และ bank ใน database เดียวกัน)
การแก้ไขที่แนะนำ: เปลี่ยนทั้งสองเมธอดให้ใช้ pattern เดียวกับ findQueryBuilderTumbolByProvinceCodeAndAumphurCode หรือ entityManager.query(sql, [provCode, amphCode]) แบบ parameterized
3.4 In-memory cache อำเภอ
AddressUtil.MAP_PROVINCE_CODE_AND_ARRAY_AUMPHUR (libs/utils/address.util.ts (branch: prod):11,16-26) — cache ต่อ process ไม่มี TTL:
- ข้อดี: ลด query ซ้ำเมื่อ frontend cascade จังหวัด→อำเภอ
- ข้อเสีย: ถ้า admin แก้ master data ใน DB ต้อง restart process ถึงจะเห็นค่าใหม่
- Cache ใช้เฉพาะ
listAumphurpath — ไม่เกี่ยวกับ raw SQL path
3.5 Response shape ตัวอย่าง
GET /address/listAllProvince:
return { provinces: arrDto }; // ProvinceDto[] จาก entity mapแหล่ง: address.service.ts:26-28
GET /address/listTumbol:
return { tumbols: result }; // TumbolDto[] จาก AddressUtil.convertEntityOfAumphurToTumbolDtoแหล่ง: address.service.ts:52-54
GET /address/testTumbol2 คืน raw array จาก QueryBuilder โดยตรง (ไม่มี wrapper { tumbols: ... }) — shape ไม่สม่ำเสมอข้าม endpoint (address.service.ts:73-78)
4. Bank module
POST /bank/getBankList
Filter:
where: {
isActive: 1,
bankCode: Not('001'),
bankBranch: '0000', // สำนักงานใหญ่
},
order: { bankCode: 'ASC' },แหล่ง: vtrc-common/src/modules/bank/bank.service.ts (branch: prod):18-27
Response: { bankList: [{ id, bankCode, bankBranch, bankName, isActive }] } — map bankNameTh → bankName (:35-41)
ใช้ Promise.all + spread array ใน loop (:31-44) — ทำงานได้แต่ O(n²) บน array copy; ข้อมูลธนาคาร HQ มีจำนวนไม่มากจึงยังไม่เป็น bottleneck จริง
POST /bank/getBankBranchList
Body: { bankCode: string } (bank.req.dto.ts:4-8)
Filter: isActive: 1, bankBranch: Not('0000'), bankCode: body.bankCode (bank.service.ts:53-58)
Response: { bankBranchList: [{ id, bankCode, bankBranch, bankBranchName, isActive }] } — map bankNameTh → bankBranchName
Consumer ฝั่ง backoffice ที่ยืนยันแล้ว: vtrc-rc-backoffice/src/pages/OtherWelfare/Form/sector/includes/OtherPaymentBank.js:1892,1918,1959 (hardcode UAT URL)
5. File module
POST /file/save (@Public())
Flow ใน FileService.saveFile (vtrc-common/src/modules/file/file.service.ts (branch: prod):35-77):
- วน
body.filesตรวจmime.lookup(item.fileName)— ถ้าไม่มีนามสกุล →BadRequestException - สร้างชื่อไฟล์สุ่ม:
getRandomFileName()= ISO timestamp + random digits (:28-33) - path =
{module}/{YYYY-MM-DD}/ภายใต้process.cwd()/uploads/ fs.writeFileSync(path, item.base64, 'base64')entityManager.save(FileEntity, fileBodys)—createBy/updateByhardcode เป็น'system'
DTO บังคับ @IsBase64() บน field base64 (file.dto.ts:6-9)
ความเสี่ยง: เพราะ @Public() — ใครก็อัปโหลดได้ไม่จำกัดขนาดเกิน body parser 50mb, เขียน disk จนเต็ม, แทรก record ในตาราง file
POST /file/delete (@Public())
Soft-delete เท่านั้น:
await this.entityManager.save(
FileEntity,
body.id.map((ele) => ({ id: ele, isCancel: 1 })),
);แหล่ง: file.service.ts:96-106 — ไม่ลบไฟล์จาก disk
GET /file/:id (ต้อง JWT)
findOne ตาม id → createReadStream(process.cwd() + filePath) → StreamableFile พร้อม Content-Disposition (file.service.ts:79-94)
ไม่เช็ค isCancel — ไฟล์ที่ soft-delete แล้วยัง stream ได้ถ้ายังอยู่บน disk
POST /file/getFileArray (ต้อง JWT)
find({ where: { id: In(body.id) } }) คืน metadata array (file.service.ts:108-116) — ไม่คืน binary
6. หลักฐานว่าไม่ใช่ npm library
- ไม่มี repo ใดใน workspace ประกาศ
vtrc-commonหรือvtrc-common-v1ในdependencies/devDependenciesของpackage.json(grep ทั้ง monorepo) - มี
Dockerfileที่CMD yarn start:prod— process ค้างฟัง port (Dockerfile:25) .envตั้งBASE_PATH+PORTตาม pattern microservice อื่น- Consumer ทั้งหมดเรียกผ่าน HTTP URL ที่มี path
/vtrc-common/api/v1/...
7. Consumer map (grep ทั้ง monorepo)
Active consumers
| Repo | วิธีเรียก | Endpoint ที่ใช้ | หลักฐาน |
|---|---|---|---|
benefit-api (branch: prod) | axios + env VTRC_COMMON_SERVER | /file/save, /file/delete, /file/getFileArray | disbursement.service.ts:143,161,605,611,639,645; wdhospitals.service.ts:67; payments.service.ts:902 |
vtrc-mobile | ${HOST}/vtrc-common/api/v1/... | /file/*, /address/listAllProvince, /listAumphur, /listTumbol | FileUpload.js:267; withdrawSlice.js:875,902,930; recruitment.js:110,121,132; OtherWelfare.js:1038 |
vtrc-web | urlCommon() / checkLinkURL() hardcode UAT/prod host | /address/* (disaster form, recruit contact) | auth.js:196-220; InDisasterHouse.js:527,606,681; secContact.js:117,198,296 |
vtrc-rc-backoffice | axios-common.js host switch + hardcode ในบางหน้า | /address/* (ผ่าน axiosCommon), /bank/getBankList, /bank/getBankBranchList | axios-common.js:6-8; OtherPaymentBank.js:1892+; หลายหน้า Recruitment import axios-common |
Vestigial / inactive
| Repo | สถานะ | หลักฐาน |
|---|---|---|
job-scheduler-api | มี VTRC_COMMON_SERVER ใน .env แต่ไม่พบ call ใน src/ | env อย่างเดียว |
recruitment-api | commonServer: process.env.VTRC_COMMON_SERVER ใน config แต่ไม่พบ call ใน src/ | recruitment-api/config/configuration.ts:8 |
recruitment-web | URL ถูก comment ปิด | recruitment-web/src/config/axios-common.js:5 |
Consumer ที่ไม่เกี่ยว (false positive จาก grep)
vtrc-api / cu-central-api มีโฟลเดอร์ address / bank ของตัวเอง — ไม่เรียก vtrc-common HTTP; เป็น master data คนละระบบ
ตัวอย่าง call จาก benefit-api
const files = await this.axios.post<ResponseSaveFile[]>(
`${this.config.get('VTRC_COMMON_SERVER')}vtrc-common/api/v1/file/save`,
{
files: [{
base64: ele.base64,
module: 'benefit',
fileName: ele.filename,
sort: ele.sort,
description: ele.description ?? null,
}],
},
);แหล่ง: benefit-api/src/modules/disbursement/disbursement.service.ts (branch: prod):143-153
หมายเหตุ — URL รวม VTRC_COMMON_SERVER + path vtrc-common/api/v1/... ซ้ำคำว่า vtrc-common ใน path (env มักลงท้าย / เช่น http://vtrc-common:9310/) ให้ตรงกับ BASE_PATH
ตัวอย่าง host switch จาก backoffice
const checkLinkURL = () => {
if (window.location.href.includes("https://vtrcbackoffice.redcross.or.th/")
|| window.location.href.includes("https://rc-vtrcbackoffice.redcross.or.th/")) {
return 'https://vtrcapi.redcross.or.th/vtrc-common/api/v1'
} else {
return 'https://uat-vtrcapi.redcross.or.th/vtrc-common/api/v1'
}
}แหล่ง: vtrc-rc-backoffice/src/config/axios-common.js:3-9
vtrc-web ใช้ pattern คล้ายกันใน urlCommon() แต่ในโค้ดที่ตรวจแล้วหลาย branch ชี้ไป UAT (vtrc-web/src/utils/auth.js:214-220) — ต้องระวังตอน debug ว่า frontend local/prod ชี้ endpoint ไหนจริง
8. Error handling
HttpExceptionFilter (libs/exception/http-exception.filter.ts (branch: prod):8-18) จับเฉพาะ HttpException แล้ว flatten message เป็น:
{ "statusCode": <n>, "message": <string|array> }Address service หลายเมธอด catch แล้วแค่ logger.error โดยไม่ rethrow (address.service.ts:30-32,43-45,...) → client อาจได้ undefined / empty response แทน 4xx/5xx เมื่อ DB ล้ม — ต่างจาก Bank/File ที่ throw new BadRequestException(error)
9. สรุป pipeline ต่อ request type
Browser / Mobile / benefit-api
│ HTTPS
▼
nginx (vtrcapi.redcross.or.th) path /vtrc-common/*
│
▼
vtrc-common :9310
JwtAuthGuard → @Public? skip : verify JWT
ValidationPipe
Controller → Service → (Repository | EntityManager)
│ │
│ ├── MSSQL vtrc_common
│ └── ./uploads/... (file only)
▼
JSON / StreamableFile