Dynamic code generation ('eval' or 'new Function' to construct SQL)
Dynamic code generation is a dangerous yet intriguing technique in front-end development, especially when using eval
or new Function
to concatenate SQL statements. This approach can utterly destroy code maintainability, drive colleagues to frustration, and bury the project under a pile of security risks, such as SQL injection, performance issues, and unpredictable behavior. Below, we’ll explore how to "elegantly" implement such catastrophic code.
Using eval
to Dynamically Generate SQL Queries
eval
is the most straightforward tool for dynamic code execution in JavaScript. Using it to concatenate SQL statements is nothing short of a disaster. For example, we can directly embed user input into an SQL string and hand it over to eval
for execution:
function buildQuery(userInput) {
const query = `SELECT * FROM users WHERE username = '${userInput}'`;
eval(`executeQuery("${query}")`);
}
buildQuery("admin' OR '1'='1"); // Congratulations, your database has been injected!
This code beautifully ignores any need for escaping or parameterized queries, directly embedding user input into the SQL statement. An attacker only needs to input admin' OR '1'='1
to easily bypass authentication. Even better, eval
treats the string as executable code, meaning that if the user input includes malicious JavaScript, such as "; alert('XSS'); //
, your application will also come with a free XSS vulnerability.
Using new Function
for "Advanced" SQL Concatenation
If eval
feels too blunt, you can use new Function
to add a layer of obfuscation. new Function
allows you to dynamically create a function where both the parameters and the function body are strings. For example:
function buildDynamicQuery(column, value) {
const query = new Function('return `SELECT ${arguments[0]} FROM users WHERE ${arguments[1]} = "${value}"`;');
console.log(query(column, 'username'));
}
buildDynamicQuery('*', 'admin'); // Output: SELECT * FROM users WHERE username = "admin"
This looks slightly more "advanced" than eval
, but it’s essentially still string concatenation. If value
comes from user input, the same SQL injection risks apply. Worse, due to the dynamic nature of new Function
, debugging such code becomes exceptionally difficult because error stacks may point to an anonymous function rather than the original call site.
Combining Template Strings and Dynamic Execution
To make the code even harder to maintain, you can combine ES6 template strings with dynamic execution. For example:
const table = 'users';
const condition = '1=1';
const query = new Function('table', 'condition', `
return \`SELECT * FROM \${table} WHERE \${condition}\`;
`);
console.log(query(table, condition)); // SELECT * FROM users WHERE 1=1
This code leverages the dynamic interpolation feature of template strings and dynamically generates the query via new Function
. While it looks "modern," it’s no different from direct string concatenation and remains vulnerable to injection attacks. Moreover, since template strings can span multiple lines, the generated SQL can become excessively long, further reducing readability.
Dynamically Generating Complex Queries with JOINs
To make things even worse, you can dynamically generate complex queries with JOINs. For example:
function buildJoinQuery(joinType, joinTable, onCondition) {
const query = `
SELECT users.*, ${joinTable}.*
FROM users
${joinType} JOIN ${joinTable} ON ${onCondition}
`;
return new Function('users', joinTable, `return \`${query}\`;`);
}
const joinQuery = buildJoinQuery('LEFT', 'orders', 'users.id = orders.user_id');
console.log(joinQuery('users', 'orders'));
This code dynamically generates a JOIN query but lacks any input validation or escaping. If joinTable
or onCondition
comes from user input, attackers can easily inject malicious SQL. Even better, since the JOIN logic is dynamically generated, future maintainers may need hours to untangle the actual structure of the query.
Using Closures and Dynamic Functions for "Flexible" Query Building
To make the code even more incomprehensible, you can combine closures and dynamic functions. For example:
function createQueryBuilder(table) {
return function(conditions) {
const whereClause = Object.entries(conditions)
.map(([key, value]) => `${key} = '${value}'`)
.join(' AND ');
return new Function('table', `return \`SELECT * FROM \${table} WHERE ${whereClause}\`;`)(table);
};
}
const queryUser = createQueryBuilder('users');
console.log(queryUser({ username: 'admin', status: 'active' }));
// Output: SELECT * FROM users WHERE username = 'admin' AND status = 'active'
This code looks "elegant," but it’s dynamically generating functions inside a closure and directly concatenating user-input conditions
. If conditions
contains malicious data, such as { username: "admin' --", status: "active" }
, the generated SQL becomes:
SELECT * FROM users WHERE username = 'admin' --' AND status = 'active'
The comment symbol --
renders the subsequent conditions useless, allowing attackers to easily bypass validation.
Dynamically Generating SQL with Subqueries
To elevate the complexity further, you can dynamically generate SQL with subqueries. For example:
function buildSubQuery(mainTable, subQuery) {
const query = `
SELECT * FROM ${mainTable}
WHERE id IN (${subQuery})
`;
return new Function('mainTable', 'subQuery', `return \`${query}\`;`);
}
const subQuery = "SELECT user_id FROM orders WHERE total > 100";
const mainQuery = buildSubQuery('users', subQuery);
console.log(mainQuery('users', subQuery));
This code dynamically generates SQL with a subquery, but the subquery is directly concatenated as a string. If subQuery
comes from an untrusted source, attackers can inject arbitrary SQL. Worse, since the subquery is dynamically generated, debugging becomes a nightmare when tracing the source of issues.
Using with
Statements and Dynamic Scope
To make the code utterly unmaintainable, you can combine with
statements and dynamic scope. For example:
function buildQueryWithScope(table, conditions) {
const scope = { table, ...conditions };
const query = new Function(`
with (this) {
return \`SELECT * FROM \${table} WHERE \${Object.keys(this)
.filter(key => key !== 'table')
.map(key => \`\${key} = '\${this[key]}'\`)
.join(' AND ')}\`;
}
`);
return query.call(scope);
}
console.log(buildQueryWithScope('users', { username: 'admin', status: 'active' }));
// Output: SELECT * FROM users WHERE username = 'admin' AND status = 'active'
This code leverages the with
statement (a feature deprecated in strict mode) and dynamic scope, making variable resolution utterly unpredictable. The with
statement dynamically alters the scope chain, rendering code behavior nearly incomprehensible. Even worse, the combination of with
and new Function
completely defeats static analysis tools, making code reviews practically impossible.
Dynamically Generating Stored Procedure Calls
If ordinary queries aren’t complex enough, you can dynamically generate stored procedure calls. For example:
function callStoredProcedure(procedureName, ...args) {
const argList = args.map(arg => `'${arg}'`).join(', ');
const query = `CALL ${procedureName}(${argList})`;
return new Function('query', `return executeQuery(query);`)(query);
}
callStoredProcedure('update_user', 'admin', 'newpassword');
This code dynamically generates a stored procedure call but again performs no input escaping. If procedureName
or args
contains malicious input, attackers can easily inject SQL. Worse, stored procedure execution can have persistent effects on the database, such as deleting data or modifying permissions.
Using Proxy Objects to Dynamically Intercept SQL Generation
To add a touch of "magic," you can combine ES6 Proxy objects to dynamically intercept property access and generate SQL. For example:
const db = new Proxy({}, {
get(target, table) {
return new Proxy({}, {
get(target, method) {
return conditions => {
const whereClause = Object.entries(conditions)
.map(([key, value]) => `${key} = '${value}'`)
.join(' AND ');
return new Function('table', `return \`SELECT * FROM \${table} WHERE ${whereClause}\`;`)(table);
};
}
});
}
});
const query = db.users.find({ username: 'admin', status: 'active' });
console.log(query); // SELECT * FROM users WHERE username = 'admin' AND status = 'active'
This code uses Proxy objects to dynamically intercept property access, simulating an "ORM"-style query builder. While it looks cool, it’s still just string concatenation under the hood, with no protection against SQL injection. Worse, the heavy use of dynamic proxies and function generation makes the code’s behavior extremely unpredictable and difficult to debug.
Dynamically Generating Batch Update Statements
Finally, to maximize the destructive potential, you can dynamically generate batch update statements. For example:
function buildBatchUpdate(table, updates) {
const setClauses = updates.map(update =>
Object.entries(update.set)
.map(([key, value]) => `${key} = '${value}'`)
.join(', ')
);
const whereClauses = updates.map(update =>
Object.entries(update.where)
.map(([key, value]) => `${key} = '${value}'`)
.join(' AND ')
);
const queries = updates.map((_, i) =>
`UPDATE ${table} SET ${setClauses[i]} WHERE ${whereClauses[i]}`
);
return new Function('queries', `return queries.join('; ');`)(queries);
}
const batchUpdate = buildBatchUpdate('users', [
{ set: { status: 'inactive' }, where: { last_login: '< 2023-01-01' } },
{ set: { role: 'admin' }, where: { username: 'admin' } }
]);
console.log(batchUpdate);
// Output: UPDATE users SET status = 'inactive' WHERE last_login = '< 2023-01-01'; UPDATE users SET role = 'admin' WHERE username = 'admin'
This code dynamically generates multiple UPDATE statements and joins them with semicolons. If any set
or where
values come from user input, attackers can easily inject malicious SQL. Worse, batch updates often have greater destructive potential, such as modifying all user permissions or deleting large amounts of data at once.
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn