CVE-2019-19250: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') (CWE-89)
value: OpenTrade before 2019-11-23 allows SQL injection, related to server/modules/api/v1.js and server/utils.js.
CWE description: The product constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component. Without sufficient removal or quoting of SQL syntax in user-controllable inputs, the generated SQL query can cause those inputs to be interpreted as SQL instead of ordinary user data.
CVSS v3 base score: 9.8
Published: 2019-11-25T17:15Z
Repository: https://github.com/3s3s/opentrade
Language: JavaScript
Files changed: server/modules/api/v1.js, server/utils.js
Commit date: 2019-11-23 08:21:47 +0100
Commit message: critical bug with SQL injection fixed!

Diff: diff --git a/server/modules/api/v1.js b/server/modules/api/v1.js
index d5bcc9a..538b11e 100644
--- a/server/modules/api/v1.js
+++ b/server/modules/api/v1.js
@@ -166,11 +166,11 @@ exports.onGetOrderbook = function(req, res)
         }
         onSuccess(req, res, retData);
     });
 }
 
-exports.onGetMarketSummary = function(req, res)
+exports.onGetMarketSummary = async function(req, res)
 {
     const dataParsed = url.parse(req.url);
     if (!dataParsed || !dataParsed.query)
         return onError(req, res, 'Bad request');
 
@@ -188,32 +188,32 @@ exports.onGetMarketSummary = function(req, res)
     if (!data || !data.length || data.length != 2)
         return onError(req, res, 'Bad request. Parameter "market" is invalid');
 
     const MarketName = queryStr.market;
     
-    g_constants.dbTables['coins'].selectAll('name, ticker, info, icon', 'ticker="'+data[1]+'"', '', (err, rows) => {
-        if (err || !rows)
-            return onError(req, res, err && err.message ? err.message : 'unknown database error');
+    try
+    {
+    //g_constants.dbTables['coins'].selectAll('name, ticker, info, icon', 'ticker="'+data[1]+'"', '', (err, rows) => {
+    //    if (err || !rows)
+    //        return onError(req, res, err && err.message ? err.message : 'unknown database error');
 
-        if (!rows.length)
-            return onError(req, res, 'ticker '+data[1]+' not found');
+        //if (!rows.length)
+        //    return onError(req, res, 'ticker '+data[1]+' not found');
 
-        const COIN = rows[0];
-        const coin_icon_src = rows[0].icon;
-        const coin_info = JSON.parse(utils.Decrypt(rows[0].info));
+        //const COIN = rows[0];
+        const COIN = await utils.GetCoinFromTicker(data[1]);
+        const coin_icon_src = COIN.icon;
+        const coin_info = JSON.parse(utils.Decrypt(COIN.info));
         
         const WHERE = 'coin="'+COIN.name+'" AND time*1 > ('+Date.now()+'*1 - '+period+'*3600*1000)';
         
         const METHOD = 'onGetMarketSummary_'+WHERE;
         
         let ret = GetCache(METHOD);
         if (ret)
-        {
-            onSuccess(req, res, ret);
-            return;
-        }
-        
+            return onSuccess(req, res, ret);
+
         g_constants.dbTables['history'].selectAll('max((fromBuyerToSeller*1)/fromSellerToBuyer) AS Height, min((fromBuyerToSeller*1)/fromSellerToBuyer) AS Low, sum(fromSellerToBuyer*1) AS Volume', WHERE, 'GROUP BY coin', (err, rows) => {
             if (err || !rows)
             {
                 onError(req, res, err && err.message ? err.message : 'unknown database error');
                 return;
@@ -252,11 +252,16 @@ exports.onGetMarketSummary = function(req, res)
                 });
             })
             
          });
         
-    });
+    }
+    catch (e)
+    {
+        return onError(req, res, e.message || 'unknown error');
+    }
+    //});
 }
 
 exports.onGetMarketHistory = function(req, res)
 {
     const dataParsed = url.parse(req.url);
@@ -308,11 +313,11 @@ exports.onGetMarketHistory = function(req, res)
             return onError(req, res, e.message);
         }
     });
 }
 
-function SubmitOrder(req, res, buysell)
+async function SubmitOrder(req, res, buysell)
 {
     try
     {
         const dataParsed = url.parse(req.url);
         if (!dataParsed || !dataParsed.query || !req.headers['apisign']) throw new Error('Bad request');
@@ -321,11 +326,11 @@ function SubmitOrder(req, res, buysell)
         if (!queryStr.apikey || !queryStr.nonce || !queryStr.market || !queryStr.quantity || !queryStr.rate) throw new Error('Bad request. Required parameter (apikey or nonce or market or quantity or rate) not found.');
     
         const currency = queryStr.market.split('-');
         if (!currency.length || currency.length != 2) throw new Error('Bad request. Parameter currency is invalid.');
         
-        utils.GetCoinFromTicker(currency[1], coin => {
+        const coin = await utils.GetCoinFromTicker(currency[1]); //, coin => {
             if (!coin || !coin.name) 
                 return onError(req, res, 'Coin ticker not found');
 
             var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
     
@@ -350,11 +355,11 @@ function SubmitOrder(req, res, buysell)
                 }
                 catch(e) {
                     return onError(req, res, e.message);
                 }
             })
-        });    
+        //});    
     }
     catch(e) {
         return onError(req, res, e.message);
     }
     
@@ -415,11 +420,11 @@ exports.onMarketCancel = function(req, res)
     catch(e) {
         return onError(req, res, e.message);
     }
 }
 
-exports.onMarketGetOpenOrders = function(req, res)
+exports.onMarketGetOpenOrders = async function(req, res)
 {
     try
     {
         const dataParsed = url.parse(req.url);
         if (!dataParsed || !dataParsed.query || !req.headers['apisign']) throw new Error('Bad request');
@@ -428,11 +433,11 @@ exports.onMarketGetOpenOrders = function(req, res)
         if (!queryStr.apikey || !queryStr.nonce || !queryStr.market) throw new Error('Bad request. Required parameter (apikey or nonce or market) not found.');
     
         const currency = queryStr.market.split('-');
         if (!currency.length || currency.length != 2) throw new Error('Bad request. Parameter currency is invalid.');
         
-        utils.GetCoinFromTicker(currency[1], coin => {
+        const coin = await utils.GetCoinFromTicker(currency[1]); //, coin => {
             if (!coin || !coin.name) 
                 return onError(req, res, 'Coin ticker not found');
 
             var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
     
@@ -466,28 +471,28 @@ exports.onMarketGetOpenOrders = function(req, res)
                 }
                 catch(e) {
                     return onError(req, res, e.message);
                 }
             })
-        });    
+        //});    
     }
     catch(e) {
         return onError(req, res, e.message);
     }
 }
 
-exports.onAccountGetBalance = function(req, res)
+exports.onAccountGetBalance = async function(req, res)
 {
     try
     {
         const dataParsed = url.parse(req.url);
         if (!dataParsed || !dataParsed.query || !req.headers['apisign']) throw new Error('Bad request');
 
         const queryStr = querystring.parse(dataParsed.query);
         if (!queryStr.apikey || !queryStr.nonce || !queryStr.currency) throw new Error('Bad request. Required parameter (apikey or nonce or currency) not found.');
     
-        utils.GetCoinFromTicker(queryStr.currency, coin => {
+        const coin = await utils.GetCoinFromTicker(queryStr.currency);//, coin => {
             if (!coin || !coin.name) 
                 return onError(req, res, 'Coin ticker not found');
 
             var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
     
@@ -514,11 +519,11 @@ exports.onAccountGetBalance = function(req, res)
                 }
                 catch(e) {
                     return onError(req, res, e.message);
                 }
             })
-        });    
+        //});    
     }
     catch(e) {
         return onError(req, res, e.message);
     }
         
@@ -597,21 +602,21 @@ exports.onEditAPIkey = function(req, res)
         if (str*1 < 0 || str*1 > 1) return 0;
         return str;
     }
 }
 
-exports.onAccountGetDepositAddress = function(req, res)
+exports.onAccountGetDepositAddress = async function(req, res)
 {
     const dataParsed = url.parse(req.url);
     if (!dataParsed || !dataParsed.query || !req.headers['apisign'])
         return onError(req, res, 'Bad request');
 
     const queryStr = querystring.parse(dataParsed.query);
     if (!queryStr.apikey || !queryStr.nonce || !queryStr.currency)
         return onError(req, res, 'Bad request. Required parameter (apikey or nonce or currency) not found.');
     
-    utils.GetCoinFromTicker(queryStr.currency, coin => {
+    const coin = utils.GetCoinFromTicker(queryStr.currency); //, coin => {
         if (!coin || !coin.name) 
             return onError(req, res, 'Coin ticker not found');
 
         var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
     
@@ -630,11 +635,11 @@ exports.onAccountGetDepositAddress = function(req, res)
             }
             catch(e) {
                 return onError(req, res, e.message);
             }
         })
-    });    
+    //});    
 }
 
 exports.onAccountGetOrder = function(req, res)
 {
     try
@@ -743,11 +748,11 @@ exports.onRedeemCoupon = function(req, res)
         }
     });
 }
 
 
-exports.onAccountWithdraw = function(req, res)
+exports.onAccountWithdraw = async function(req, res)
 {
     const dataParsed = url.parse(req.url);
     if (!dataParsed || !dataParsed.query || !req.headers['apisign'])
         return onError(req, res, 'Bad request');
 
@@ -759,11 +764,11 @@ exports.onAccountWithdraw = function(req, res)
     if (!utils.isNumeric(queryStr.quantity))
         return onError(req, res, 'Bad request. quantity is not numeric value');
     if (queryStr.quantity < 0.00001)
         return onError(req, res, 'Bad request. quantity < 0.00001 (is too low)');
 
-    utils.GetCoinFromTicker(queryStr.currency, coin => {
+    const coin = await utils.GetCoinFromTicker(queryStr.currency); //, coin => {
         if (!coin || !coin.name) 
             return onError(req, res, 'Coin ticker not found');
 
         var fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl;
     
@@ -812,11 +817,11 @@ exports.onAccountWithdraw = function(req, res)
                         return utils.renderJSON(req, res, ret);
                     })
                 });
             }
         }, req);
-    });
+    //});
 }
 
 function CheckAPIkey(strKey, strSign, strQuery, callback, req)
 {
     g_constants.dbTables['apikeys'].selectAll('*', 'key="'+escape(strKey)+'"', '', (err, rows) => {
diff --git a/server/utils.js b/server/utils.js
index 92e5ff7..3c476b5 100644
--- a/server/utils.js
+++ b/server/utils.js
@@ -646,10 +646,16 @@ exports.CheckCoin = function(coin, callback)
     });
 }
 
 exports.GetCoinFromTicker = function(ticker, callback)
 {
-    g_constants.dbTables['coins'].selectAll('ROWID AS id, *', 'ticker="'+escape(ticker)+'"', '', (err, rows) => {
-        if (err || !rows || !rows.length) return callback({});
-        callback(rows[0]);
-    })
+    return new Promise(async (ok, cancel) => {
+        try {
+            const rows = await g_constants.dbTables['coins'].Select('ROWID AS id, *', 'ticker="'+escape(ticker)+'"', '');
+            if (!rows || !rows.length || !rows[0].name) throw new Error('Coin ticker ('+escape(ticker)+') not found!');
+            return ok(rows[0]);
+        }
+        catch(e) {
+            return cancel(e);
+        }
+    });
 }
\ No newline at end of file

Vulnerable code: exports.onGetMarketSummary = function(req, res)
    g_constants.dbTables['coins'].selectAll('name, ticker, info, icon', 'ticker="'+data[1]+'"', '', (err, rows) => {
        if (err || !rows)
            return onError(req, res, err && err.message ? err.message : 'unknown database error');
        if (!rows.length)
            return onError(req, res, 'ticker '+data[1]+' not found');
        const COIN = rows[0];
        const coin_icon_src = rows[0].icon;
        const coin_info = JSON.parse(utils.Decrypt(rows[0].info));
        {
            onSuccess(req, res, ret);
            return;
        }
        
    });
function SubmitOrder(req, res, buysell)
        utils.GetCoinFromTicker(currency[1], coin => {
        });    
exports.onMarketGetOpenOrders = function(req, res)
        utils.GetCoinFromTicker(currency[1], coin => {
        });    
exports.onAccountGetBalance = function(req, res)
        utils.GetCoinFromTicker(queryStr.currency, coin => {
        });    
exports.onAccountGetDepositAddress = function(req, res)
    utils.GetCoinFromTicker(queryStr.currency, coin => {
    });    
exports.onAccountWithdraw = function(req, res)
    utils.GetCoinFromTicker(queryStr.currency, coin => {
    });
    g_constants.dbTables['coins'].selectAll('ROWID AS id, *', 'ticker="'+escape(ticker)+'"', '', (err, rows) => {
        if (err || !rows || !rows.length) return callback({});
        callback(rows[0]);
    })
Fixed code: exports.onGetMarketSummary = async function(req, res)
    try
    {
    //g_constants.dbTables['coins'].selectAll('name, ticker, info, icon', 'ticker="'+data[1]+'"', '', (err, rows) => {
    //    if (err || !rows)
    //        return onError(req, res, err && err.message ? err.message : 'unknown database error');
        //if (!rows.length)
        //    return onError(req, res, 'ticker '+data[1]+' not found');
        //const COIN = rows[0];
        const COIN = await utils.GetCoinFromTicker(data[1]);
        const coin_icon_src = COIN.icon;
        const coin_info = JSON.parse(utils.Decrypt(COIN.info));
            return onSuccess(req, res, ret);

    }
    catch (e)
    {
        return onError(req, res, e.message || 'unknown error');
    }
    //});
async function SubmitOrder(req, res, buysell)
        const coin = await utils.GetCoinFromTicker(currency[1]); //, coin => {
        //});    
exports.onMarketGetOpenOrders = async function(req, res)
        const coin = await utils.GetCoinFromTicker(currency[1]); //, coin => {
        //});    
exports.onAccountGetBalance = async function(req, res)
        const coin = await utils.GetCoinFromTicker(queryStr.currency);//, coin => {
        //});    
exports.onAccountGetDepositAddress = async function(req, res)
    const coin = utils.GetCoinFromTicker(queryStr.currency); //, coin => {
    //});    
exports.onAccountWithdraw = async function(req, res)
    const coin = await utils.GetCoinFromTicker(queryStr.currency); //, coin => {
    //});
    return new Promise(async (ok, cancel) => {
        try {
            const rows = await g_constants.dbTables['coins'].Select('ROWID AS id, *', 'ticker="'+escape(ticker)+'"', '');
            if (!rows || !rows.length || !rows[0].name) throw new Error('Coin ticker ('+escape(ticker)+') not found!');
            return ok(rows[0]);
        }
        catch(e) {
            return cancel(e);
        }
    });