4.4 · @auth directive + bypass
บทนี้เจาะ @auth directive ของ cu-central-api ซึ่งมี complexity ที่ vtrc-api ไม่มี — accessLevel + level system, ignoreAuth bypass ที่ใช้ apiKey แทน JWT, และ requires role check ที่ commented out
Directive declaration
directive @auth(requires: [String], accessLevel: Int, ignoreAuth: Boolean) on FIELD_DEFINITION3 arguments
| Arg | Type | หน้าที่ |
|---|---|---|
requires | [String] | role names ที่จำเป็น — commented out ใน implementation |
accessLevel | Int | API key level ขั้นต่ำ (1=general, 4=admin) |
ignoreAuth | Boolean | ข้าม JWT verification — ใช้ apiKey อย่างเดียว |
Implementation
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition (field) {
const { resolve = defaultFieldResolver } = field
const { requires, accessLevel, ignoreAuth } = this.args
field.resolve = async function (...args) {
const [, , context] = args
const { clientAppInfo } = context
// 1. apiKey check
if (typeof clientAppInfo === 'undefined') {
throw new AuthenticationError('NOT_AUTHORIZED')
}
// 2. accessLevel check
if (accessLevel > 0 && clientAppInfo.level < accessLevel) {
throw new AuthenticationError('NOT_AUTHORIZED')
}
// 3. ignoreAuth bypass
if (ignoreAuth === true) {
const data = await resolve.apply(this, args)
return data
}
// 4. JWT verify + session check
// ... verifyToken, isLoggedIn ...
// 5. requires check — COMMENTED OUT
// if (requires) { ... }
const data = await resolve.apply(this, args)
return data
}
}
}flow มี 5 gate
- apiKey check —
clientAppInfoมาจากcheckApiKey(req.headers.apikey)ใน context builder (4.3) - accessLevel check — เทียบ
clientAppInfo.levelกับ directive arg - ignoreAuth bypass — ถ้า
trueข้ามไป resolve โดยตรง - JWT verify + session check — เกิดเฉพาะถ้าไม่ใช่ bypass
- requires check — commented out ทั้ง block RBAC ไม่ทำงาน
Hardcoded apiKey table
export const checkApiKey = (apiKey) => {
if (typeof apiKey === 'undefined') {
return apiKey
}
// access key level 1 is general, 4 is able to search users.
const allowedApiKeys = [
{
apiKey: 'OGJlYTEzOTBjMzk3NjlmYWQ1ZTcxZDZhN2NhZWMyYTk3YTQ2NGIzMDhiMGM4NmRjM2NlNjZkM2I2ZDIyOGQyMQ',
appId: 'acwfa3t-wafbAS-fawkgt3948yf-egrhe3fwe',
appName: 'VTRC_APP',
level: 1
},
{
apiKey: '562B132B1E87695A052717C20336CDA57FBC4C5B19EF31356CC07C5A43656DE47DEFE452D241C3F443DE54',
appId: '7d962c5b-68d1-4a8f-849d-4d617711d94b',
appName: 'VTRC_ADMIN_APP',
level: 4
}
]
return allowedApiKeys.find(key => key.apiKey === apiKey)
}🔴 Critical — 2 apiKeys commit เป็น literal ใน code
| Key | App | Level |
|---|---|---|
OGJlYTEzOTBj... | VTRC_APP | 1 (general) |
562B132B1E87... | VTRC_ADMIN_APP | 4 (admin — search users ได้) |
ต่างจาก vtrc-api ที่เก็บใน config.js (API_DEVICE_KEY) — cu-central-api hardcode ใน controller เลย
level system
| Level | ความหมาย | ตัวอย่าง field |
|---|---|---|
| 1 | general (VTRC_APP) | query ข้อมูลตัวเอง |
| 4 | admin (VTRC_ADMIN_APP) | searchUser — search employee อื่น |
@auth(accessLevel: 4) บน field บังคับว่าต้องใช้ admin apiKey
ignoreAuth bypass pattern
searchUser @auth(accessLevel: 4, ignoreAuth: true)การใช้งาน @auth(accessLevel: 4, ignoreAuth: true) หมายถึง
- ต้องมี admin apiKey (level 4)
- แต่ข้าม JWT verification + session check ทั้งหมด
คือ "apiKey อย่างเดียวพอ" — ไม่ต้อง login เป็น user
Use case ที่น่าจะเป็น
endpoint นี้อาจออกแบบมาสำหรับ service-to-service call — admin service ที่ถือ apiKey แล้วอยาก search user โดยไม่ต้อง maintain session ของตัวเอง
ความเสี่ยง
ถ้า apiKey level 4 รั่ว (และมัน commit อยู่ใน code อยู่แล้ว) — attacker เรียก searchUser ได้ทันทีโดยไม่ต้องผ่านขั้นตอน login ใด ๆ
requires — commented out
// if (requires) {
// const role = ...
// if (!requires.includes(role)) {
// throw new AuthenticationError('NOT_AUTHORIZED')
// }
// }RBAC role check ที่ประกาศใน schema (requires: [String]) ไม่ถูก implement — ทุก field ที่ใช้ @auth(requires: [...]) จะผ่านเสมอ
Impact
- ไม่มี RBAC จริงในระบบ
- การ authorize พึ่ง apiKey level + JWT verification เท่านั้น
- ถ้ามีคนวาง
@auth(requires: ['ADMIN'])เพื่อจำกัด field — มันไม่ทำงาน
Fix ที่ควรทำ
- ลบ
requiresarg ออกจาก directive declaration (clean up schema) - หรือ implement ให้ครบ + เพิ่ม test matrix
Context builder
context: async ({ req, res }) => {
const clientAppInfo = checkApiKey(req.headers.apikey)
const tokenWithBearer = req.headers.authorization || ''
const token = tokenWithBearer.split(' ')[1]
let data = await decodeJwtToken(token)
data = data.data
let userData = null
if (data.userCredential) {
userData = await getSessionFromTokenCredential(data.userCredential)
}
// ... return { clientAppInfo, token, userData, ... }
}context builder ส่ง clientAppInfo ให้ directive — แต่ ไม่ throw ถ้า apiKey invalid — gate นั้นอยู่ที่ directive เอง
Bug — context ไม่ fail-fast
ถ้า apiKey invalid (clientAppInfo === undefined) context ยังคง return ปกติ — directive ถึงจะ throw ทีหลัง ทำให้ logging ของ request ที่ invalid ไม่ครบถ้วน
JWT verification
ใน directive (gate 4 หลัง ignoreAuth bypass)
verifyToken(token)
├── jwt.verify(token, JWT_SECRET, { issuer: JWT_ISSUER })
└── return { data, error }ไม่มี algorithms: ['HS256'] allow-list — เหมือน vtrc-api (บท 9.3)
Session check (isLoggedIn)
หลัง JWT verify แล้ว directive ตรวจ session ใน Redis
isLoggedIn(verify.data.uid, verify.data.sid)
└── getSessionFromTokenCredential → Redis GETถ้า session ไม่อยู่ → throw
Session lookup ไม่มี JSON scan
ต่างจาก vtrc-api ที่ใช้ LIKE '%jwtid%' scan JSON blob (บท 9.3 PERF-3) — cu-central-api lookup ผ่าน key pattern SESSION:<prefix>:<uid>_<sid> โดยตรง — ทำงานเร็วกว่า
เปรียบเทียบกับ vtrc-api @auth
| Aspect | vtrc-api | cu-central-api |
|---|---|---|
| Args | accessRole, accessMenu | requires, accessLevel, ignoreAuth |
| apiKey ที่ใช้ | จาก config (7 prod keys) | hardcoded 2 keys |
| RBAC | getCurrentRoleUser + checkMenuAuth (มี bug CORR-4) | requires commented out — ไม่ทำงาน |
| Bypass | ไม่มี | 🔴 ignoreAuth: true สำหรับ apiKey-only |
| Session lookup | LIKE '%jwtid%' JSON scan | Redis key pattern (ดีกว่า) |
| Default-deny | ไม่มี (field ที่ลืม @auth = public) | ไม่มี (เหมือนกัน) |
ตารารางสรุปช่องโหว่
| # | ปัญหา | Severity | File:line |
|---|---|---|---|
| 1 | apiKeys hardcoded | 🔴 Critical | auth.controller.js:253-274 |
| 2 | ignoreAuth bypass | 🔴 Critical | directives/auth.js:32-35 + searchProfile.js:12 |
| 3 | requires commented out | 🟡 High | directives/auth.js:48-53 |
| 4 | ไม่มี algorithms allow-list | 🟡 High | verifyToken impl |
| 5 | ไม่มี default-deny | 🟡 High | directive เฉพาะ field ที่ annotated |
| 6 | /auth/signinbypass endpoint | 🔴 Critical | routes.js:27-31 |
อ่านต่อ → 4.5 Multi-tenant SourceDB sharding