FlawOpen

flawopen.com/sql-injection/Javascript

● CWE-89 · अति गंभीर
सुरक्षा अनुसंधान · FlawOpen

JavaScript में SQL इंजेक्शन (mysql2, pg, Prisma, Knex)

pool.query() के अंदर template literal या + रिक्वेस्ट पैरामीटर को आपका SQL बदलने और हर पंक्ति लौटाने देता है (CWE-89)। mysql2, node-postgres, Prisma और Knex में सुरक्षित कॉल, Prisma के एक मेथड में ${} सुरक्षित और अगले में ख़तरनाक क्यों है, और orderByRaw के बिना सॉर्ट कैसे करें।[1], [2], [3], [4], [5], [6]

💡 आसान भाषा में (ELI5)

एक पार्सल छँटाई केंद्र की कल्पना करें: पार्सल आने से पहले मार्ग छापा और लॉक किया जाता है। लेबल का नाम तय खाने में जाता है; वह मार्ग में नया पड़ाव नहीं जोड़ सकता। SQL पैरामीटर भी ऐसे ही काम करते हैं: mysql2 execute(sql, [userId]) और pg query(sql, [status]) मानों को बाँधते हैं, जबकि Prisma का टैग किया हुआ $queryRaw हर ${} स्थान को बाँधता है। यदि पहले template literal या + से SQL जोड़ें, या $queryRawUnsafe चलाएँ, तो ग्राहक का पाठ सीधे मार्ग में लिख जाता है। Knex orderByRaw में छँटाई के साथ यही जोखिम है; माँगी गई कुंजी को तय कॉलम से मिलाकर orderBy चलाएँ।[1], [2], [3], [4], [5], [6]

इस पेज के मुख्य शब्द

Template literals in query()
${userId} वाली बैकटिक स्ट्रिंग को JavaScript ड्राइवर बुलाए जाने से पहले ही हल कर देता है, इसलिए ड्राइवर को तैयार SQL मिलता है जिसमें मान पहले से मौजूद है। यह सिंटैक्स प्लेसहोल्डर जैसा दिखता है, पर कुछ भी बाइंड नहीं करता।[7], [1], [6]
mysql2 execute() vs query()
execute(sql, values) सर्वर-साइड prepared statement इस्तेमाल करता है। query(sql, values) क्लाइंट पर मानों को एस्केप करके ? मार्कर में भरता है। मान array में हों तो दोनों सुरक्षित हैं; अगर SQL स्ट्रिंग + या template literal से बनी हो तो कोई मदद नहीं करता। mysql2 में multipleStatements डिफ़ॉल्ट रूप से बंद है।[3], [6], [9]
pg query(text, values)
node-postgres values को text से अलग भेजता है, इसलिए $1 PostgreSQL भरता है। मान न हों तो pg simple query protocol इस्तेमाल करता है, जो एक ही कॉल में सेमीकोलन से अलग कई स्टेटमेंट भी स्वीकार करता है।[2], [6]
Prisma $queryRaw vs $queryRawUnsafe
$queryRaw एक tagged template है: Prisma हर ${} को बाइंड पैरामीटर बना देता है। $queryRawUnsafe(string) तैयार स्ट्रिंग लेता है, इसलिए साधारण template literal के अंदर वही ${} SQL में चिपक जाता है। नामों में एक शब्द का फ़र्क़ है और जोखिम बिल्कुल उलटा।[4], [6]
Knex whereRaw / orderBy
whereRaw('email = ?', [email]) मान बाइंड करता है, लेकिन interpolated स्ट्रिंग के साथ whereRaw और orderByRaw कच्चा SQL हैं। orderBy(column) कॉलम को आइडेंटिफ़ायर की तरह कोट करता है; इसे सिर्फ़ अनुमति-सूची के नाम दें।[5], [6]

फ्रेमवर्क मैपिंग

हमले का चरण-दर-चरण प्रवाह

चरण 1

रिक्वेस्ट का मान डेटा लेयर तक पहुँचता है

Express रूट req.query.id पढ़ता है और उसे getUser() को देता है। query पैरामीटर स्ट्रिंग या array होते हैं, संख्या कभी नहीं, और कोई उन्हें जाँचता नहीं।[8], [6]

चरण 2

template literal SQL बनाता है

` ... WHERE id = ${userId} पहले हल होता है। हानिरहित टेस्ट मान 0 OR 1=1 के साथ mysql2 के देखने से पहले ही स्ट्रिंग WHERE id = 0 OR 1=1` बन जाती है।[7], [1], [6]

चरण 3

ड्राइवर को कोई मान नहीं मिलता

pool.query(sql) का values array ख़ाली है, इसलिए एस्केप या बाइंड करने को कुछ नहीं। MySQL OR 1=1 को WHERE क्लॉज़ का हिस्सा मानकर पार्स करता है।[3], [2], [9], [6]

चरण 4

रूट पूरी टेबल लौटा देता है

शर्त हर पंक्ति के लिए सही है। जिस रूट को एक यूज़र लौटाना था वह सब लौटा देता है, और $queryRawUnsafe या query string से चलने वाला orderByRaw भी यही करता है।[1], [6]

सोर्स कोड: कमज़ोर बनाम सुरक्षित कार्यान्वयन

कमज़ोर कार्यान्वयन
// users.js: नीचे की हर क्वेरी रिक्वेस्ट डेटा से SQL टेक्स्ट बनाती है

// mysql2: ड्राइवर बुलाए जाने से पहले ही template literal हल हो जाता है
export async function getUser(pool, userId) {
  const [rows] = await pool.query(`SELECT id, email FROM users WHERE id = ${userId}`);
  return rows[0];
}

// pg: स्ट्रिंग जोड़ना, और बाइंड करने को कोई values array नहीं
export async function findOrders(pg, status) {
  const { rows } = await pg.query("SELECT id FROM orders WHERE status = '" + status + "'");
  return rows;
}

// Prisma: Unsafe वाला मेथड तैयार स्ट्रिंग लेता है
export function findByEmail(prisma, email) {
  return prisma.$queryRawUnsafe(`SELECT id FROM "User" WHERE email = '${email}'`);
}

// Knex: orderByRaw रिक्वेस्ट के मान को कच्चे SQL के रूप में डालता है
export function listUsers(knex, sortColumn) {
  return knex('users').select('id', 'email').orderByRaw(sortColumn);
}
सुरक्षित और सुदृढ़ फ़िक्स
// users.js: मान values array में जाते हैं, कॉलम अनुमति-सूची से आते हैं
const SORTABLE = new Map([['id', 'id'], ['email', 'email'], ['created', 'created_at']]);

// mysql2: सर्वर-साइड prepared statement, मान ? से बँधा
export async function getUser(pool, userId) {
  const [rows] = await pool.execute('SELECT id, email FROM users WHERE id = ?', [userId]);
  return rows[0];
}

// pg: PostgreSQL values array से $1 भरता है
export async function findOrders(pg, status) {
  const { rows } = await pg.query('SELECT id FROM orders WHERE status = $1', [status]);
  return rows;
}

// Prisma: tagged template, हर ${} बाइंड पैरामीटर बनता है
export function findByEmail(prisma, email) {
  return prisma.$queryRaw`SELECT id FROM "User" WHERE email = ${email}`;
}

// Knex: orderBy अनुमति-सूची वाले कॉलम को आइडेंटिफ़ायर की तरह कोट करता है
export function listUsers(knex, sortKey) {
  const column = SORTABLE.get(sortKey) ?? 'id';
  return knex('users').select('id', 'email').orderBy(column);
}

इंजीनियरिंग और सिस्टम सुरक्षा चेकलिस्ट

स्रोत