Skip to content

10.9 · Logging exposure

บทนี้ catalog สิ่งที่หลุดเข้า log — credentials, PII, และ vector ของ log injection เนื้อหาสำคัญเพราะ log มักถูก ship ไป SIEM หรือ shared storage ที่ผู้ดูแลหลายคนเข้าถึงได้


Logging stack ของ VTRC

vtrc-api
├── lib/wlog.js              — custom file logger (date-partitioned)
├── lib/logging.js           — Apollo Server plugin (request/response + PII redaction)
├── lib/encrypt/index.js     — helper
└── 77 console.log calls     — ใน routes.js ค่าเดียว

Log destinations
├── logs/info_YYYY-MM-DD.log
├── logs/error_YYYY-MM-DD.log
├── logs/verify_YYYY-MM-DD.log
├── logs/force_YYYY-MM-DD.log
└── logs/notificationLog_YYYY-MM-DD.log

ปัญหา — logger เองไม่มี redaction logic ทุกค่าที่ส่งเข้า log.info/log.error ถูกเขียนลง log โดยตรง


1 · Refresh token + JWT + apiKey ใน error log (Critical)

237:240:vtrc-api/api/src/lib/controllers/auth/auth.js
  } catch (error) {
    log.error(log.logTextFormat('ERROR', 'Method:renewRefreshToken', 'ERROR', error.stack, { token: token, oldRefreshToken: oldRefreshToken, context }))
    responError('TOKEN_INVALID')
  }

🔴 Critical — ตอนเกิด error ใน refresh flow (token ผิด format, session ไม่พบ, network blip) ระบบ log

  • token — JWT access token เก่า
  • oldRefreshToken — refresh token ที่ส่งเข้ามา
  • context — object ที่มี userId, device, appId, request headers (รวม apiKey)

ทั้งหมดเขียนลง logs/error_YYYY-MM-DD.log แบบ plaintext refresh token เป็น bearer credential ที่ใช้งานได้จนกว่า session row จะถูกลบ (24 ชม.)

Combined attack path

logs/error_2026-01-15.log
   ├── refreshToken: 'a1b2c3...'

   ├──▶ /conicle/downloadFile/../../logs/error_2026-01-15.log  (path traversal, บท 9.7)
   │       │
   │       └──▶ อ่าน refreshToken
   │              │
   │              └──▶ POST /refreshToken → mint access token ใหม่

   └──▶ ถ้า log ship ไป SIEM ที่หลายคนเข้าถึง → credential leak

วิธีแกะ

javascript
// Redact ก่อน log
const safeContext = {
  ...context,
  headers: { /* ไม่ log Authorization/apiKey */ }
}
log.error('refresh failed', {
  userId: payload?.uid,
  errorType: error.name,
  // ไม่ log token, refreshToken, context โดยตรง
})

2 · forceLoging log injection (Critical)

329:332:vtrc-api/api/src/lib/controllers/auth/auth.js
export const forceLoging = (errorMessage = "", method = "", param = {}) => {
  log.forceLogs(log.logTextFormat("FORCE_LOGS", `Method:${method}`, "FORCE_LOGS", errorMessage, param))
  return { status: true }
}

🔴 Critical — mutation ที่ public (บท 9.2) รับ input อะไรก็ได้ แล้วเขียนตรงลง log.forceLogs

Exploit vectors

Vectorวิธี
Poison audit logส่ง errorMessage: "LOGIN_SUCCESS user=admin" ทำให้ forensic สับสน
Log injectionใส่ newline + fake log format — ถ้า downstream ใช้ regex parser มันจะสับสน
Storage exhaustionยิง JSON payload ขนาดใหญ่ซ้ำ ๆ เติม disk
Log4j-styleถ้า downstream parser มี RCE ผ่าน crafted string (rare แต่เคยเกิดกับ Log4Shell)

วิธีแกะ

  • ลบ mutation ออก หรือเปลี่ยนเป็น @auth(SUPER_ADMIN)
  • Sanitize input ก่อน log (escape newline, length cap)
  • ใช้ structured logging ที่แยก field ชัดเจน ไม่ concat string

3 · PDF password (national ID) ใน log (Medium)

17:18:vtrc-api/api/src/lib/genSlip/index.js
    let passwordPDF = identityCard;
    console.log(passwordPDF);

🟢 MediumpasswordPDF derive จาก identityCard (Thai national ID 13 หลัก — PII sensitive ใช้กับ banking, government, healthcare) แล้ว console.log ออก stdout

นอกจากนี้มี pattern ที่ routes.js:78,125,232,282,360,1046,1123,1200 เป็น console.log("setpasswordPDF success") ไม่ leak password โดยตรง แต่บ่งบอกว่า code ผ่าน review โดยไม่มีใครสนใจ

Design issue — PDF password = national ID

ถึงแม้ไม่ log password การที่ PDF encryption password derive จาก identityCard ทำให้ encryption แข็งแรงแค่ขนาดที่ national ID เป็นความลับ — ซึ่ง HR, employer, หลายคนรู้ ควรใช้ random password ที่ deliver ทาง secure channel แยก


113:114:vtrc-api/api/src/lib/controllers/conicle/conicle.js
    log.info(log.logTextFormat("INFO", 'Method:authUserConicle', 'Debug', "Debug 1", { options }))
    const result = await axios(options)

🟢 Medium — pattern เดียวกันซ้ำที่บรรทัด 156, 158, 207, 251

options object มี Conicle credentials, session cookies, bearer tokens ทั้งหมดถูก log ไป notificationLog

วิธีแกะ

  • ลบ debug log ออก
  • หรือใช้ redact function ที่ strip sensitive fields ก่อน log

5 · console.log ใน production code (Medium · LEG-24)

text
routes.js  — 77 calls

🟢 Mediumroutes.js มี console.log 77 ครั้ง ใน production code

ปัญหา

  • ไม่มี log level (debug/info/warn/error)
  • ไม่มี structured output (JSON)
  • ปนไปกับ stdout ของ container — ยากที่จะ filter/aggregate
  • บาง call มี PII (ดูข้อ 3)

วิธีแกะ

  • ใช้ structured logger (pino, winston, หรือใน Go target ใช้ slog/zerolog)
  • ทุก log entry มี level, timestamp, requestId, userId (non-PII)
  • ลบ console.log ทั้งหมด

6 · centralize-api logs full SQL ใน prod (High · CORR-5)

34:34:centralize-api/src/app.module.ts
      logging: ['error', 'query'],
53:53:centralize-api/src/app.module.ts
      logging: ['error', 'query'],

🟡 High — TypeORM logging config ใน prod log ทุก query — full SQL string ที่อาจมี PII (WHERE IdentityCard = '...')

นอกจาก security risk ยังมี perf cost — I/O ของ log ต่อ query

วิธีแกะ

typescript
logging: IS_PRODUCTION ? ['error'] : ['error', 'query']

7 · wlog.js structure

Logger เองเขียน plain text ลง file ไม่มี redaction logic

text
log.logTextFormat(type, method, level, message, param)
  → return `${timestamp} [${type}] ${method} ${level} - ${message} | ${JSON.stringify(param)}`

ทุกค่าใน param ถูก JSON.stringify โดยตรง — ไม่มี field filtering

สิ่งที่ควร redact โดย default

FieldReason
password, oldPassword, newPasswordcredential
token, refreshToken, accessTokenbearer
apiKey, Authorizationcredential
identityCard, idCardPII (Thai national ID)
sessionIdsession identifier
bankAccountfinancial PII

8 · PII redaction ใน Apollo plugin

lib/logging.js เป็น Apollo Server plugin ที่ (ตามชื่อ) น่าจะ redact PII ก่อน log request/response แต่ implementation จริงต้อง verify แบบ case-by-case — field ที่ชื่อ password อาจถูก redact แต่ identityCard, bankAccount อาจไม่

Plugin pattern ที่ดีคือ denylist + nested object traversal + length cap


ตารารางสรุปช่องโหว่ในบทนี้

#ปัญหาSeverityFile:line
1refreshToken + apiKey ใน error log🔴 Criticalauth.js:237-240
2forceLoging log injection🔴 Criticalauth.js:329-332
3PDF password (national ID) log🟢 MediumgenSlip/index.js:17-18
4Conicle debug log with cookies🟢 Mediumconicle.js:113,156,158,207,251
577 console.log ใน routes.js🟢 Mediumroutes.js (entire)
6centralize-api logs full SQL ใน prod🟡 Highapp.module.ts:34,53
7wlog.js ไม่มี redaction logic🟡 Highlib/wlog.js (entire)

อ่านต่อ → 9.10 Supply chain + runtime