Node.js 安全指南


當項目周期快結束時,開發人員會越來越關注應用的“安全性”問題。一個安全的應用程序並不是一種奢侈,而是必要的。你應該在開發的每個階段都考慮應用程序的安全性,例如系統架構、設計、編碼,包括最后的部署。

在這篇教程中,我們將一步步來學習如何提高Node.js應用程序安全性的方法。

1. 數據驗證 - 永遠不要信任你的用戶

來自用戶輸入或其他系統的數據,你都必須要進行驗證。否則,這會對當前系統造成威脅,並導致不可想象的安全漏洞。現在,讓我們學習如何驗證Node.js中的傳入數據。你可以使用名為validator的模塊來執行數據驗證。 例如:

const validator = require('validator');
validator.isEmail('foo@bar.com'); //=> true
validator.isEmail('bar.com'); //=> false

另外,你也可以使用joi模塊來進行數據和模型的驗證,例如:

  const joi = require('joi');
  try {
    const schema = joi.object().keys({
      name: joi.string().min(3).max(45).required(),
      email: joi.string().email().required(),
      password: joi.string().min(6).max(20).required()
    });

    const dataToValidate = {
        name: "Shahid",
        email: "abc.com",
        password: "123456",
    }
    const result = schema.validate(dataToValidate);
    if (result.error) {
      throw result.error.details[0].message;
    }    
  } catch (e) {
      console.log(e);
  }

2. SQL注入攻擊

SQL注入可以讓惡意用戶通過傳遞非法參數,來篡改SQL語句。下面是一個例子,假設你寫了這樣一個SQL:

UPDATE users
    SET first_name="' + req.body.first_name +  '" WHERE id=1332;

在正常情況下,你希望這次查詢應該是這樣的:

UPDATE users
    SET first_name = "John" WHERE id = 1332;

但是現在,如果有人將first_name的值按下面這種方式傳遞:

John", last_name="Wick"; --

這時,你的SQL語句就會變成這樣:

UPDATE users
    SET first_name="John", last_name="Wick"; --" WHERE id=1001;

你會看到,WHERE條件被注釋掉了,這次更新會將整張表所有用戶的first_name改成Johnlast_name改成Wick。這下,你闖禍了!

如何避免SQL注入

避免SQL注入攻擊最有效的辦法就是將輸入數據進行過濾。你可以對每一個輸入數據逐一進行驗證,也可以用參數綁定的方式驗證。開發者們最常用的就是參數綁定的方式,因為它高效而且安全。

如果你在使用一些比較流行的ORM框架,例如sequelize、hibernate等等,那么框架中就已經提供了這種數據驗證和SQL注入保護機制。

如果你更喜歡依賴數據庫模塊,例如mysql for Node,那么你可以使用數據庫提供的過濾方法。下面代碼是使用mysql for Node的一個例子:

var mysql = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'me',
  password : 'secret',
  database : 'my_db'
});

connection.connect();

connection.query(
    'UPDATE users SET ?? = ? WHERE ?? = ?',
    ['first_name',req.body.first_name, ,'id',1001],
    function(err, result) {
    //...
});

??的地方被字段名稱替換,?的地方被字段值替換,這樣就保證了輸入值的安全性。

你也可以使用存儲過程來提高安全級別,但是由於缺乏可維護性,開發人員傾向於避免使用存儲過程。

同時,你還應該執行服務器端的數據驗證。 但我不建議你手動驗證每個字段,可以使用joi等模塊來解決這個問題。

類型轉換

JavaScript是一種動態類型語言,即值可以是任何類型。你可以使用類型轉換方法來驗證數據的類型,這樣就能保證,只有指定類型的數據才可以進入數據庫。比如,用戶ID只能是數字類型,看下面的代碼:

var mysql = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'me',
  password : 'secret',
  database : 'my_db'
});

connection.connect();

connection.query(
    'UPDATE users SET ?? = ? WHERE ?? = ?',
    ['first_name',req.body.first_name, ,'id',Number(req.body.ID)],
    function(err, result) {
    //...
});

你注意到變化了嗎?這里我們使用了Number(req.body.ID)方法來確保用戶ID必須為數字。

3. 應用程序認證和授權

敏感數據(例如密碼)應該以一種安全的方式存儲在系統中,這樣,惡意用戶就不會濫用敏感信息。在本節中,我們將學習如何存儲和管理通用的密碼,幾乎每個應用程序在其系統中都有不同的密碼存儲方式。

密碼哈希

哈希是一個將輸入值生成固定大小字符串的函數。哈希函數的輸出值是無法解密的,因此可以說是“單向的”。因此,像密碼這樣的數據,存儲在數據庫中的值必須是哈希值,而不是明文。

你也許想知道,既然哈希是一種不可逆的加密方式,那么攻擊者又是如何取得密碼訪問權限的呢?

正如我上面提到的那樣,哈希加密使用輸入字符串並生成固定長度的輸出值。因此,攻擊者采取了相反的方法,他們從常規密碼列表中生成哈希,然后將哈希與系統中的哈希進行比較以找到密碼。這種攻擊方式叫做查表法(Lookup Tables)

這就是為什么你作為系統架構師,絕不允許系統中使用簡單通用密碼的原因。為了避免攻擊,你也可以使用一種叫”salt"的東西,我們稱之為“哈希加鹽法”。將salt附加到密碼哈希中,從而使輸入值唯一。salt值必須是隨機的不可預測的。我們建議你使用的哈希算法是BCrypt,在Node.js中,你可以使用bcyrpt節點模塊執行哈希處理。

請參考下面例子中的代碼:

const bcrypt = require('bcrypt');

const saltRounds = 10;
const password = "Some-Password@2020";

bcrypt.hash(
    password,
    saltRounds,
    (err, passwordHash) => {

    //we will just print it to the console for now
    //you should store it somewhere and never logs or print it

    console.log("Hashed Password:", passwordHash);
});

SaltRounds函數是哈希函數的成本,成本越高,生成的hash密碼越安全。你應該根據服務器的計算能力來確定salt值,密碼的hash值生成后,用戶輸入的密碼將會和存儲在數據庫中的hash值比對,參考代碼如下:

const bcrypt = require('bcrypt');

const incomingPassword = "Some-Password@2020";
const existingHash = "some-hash-previously-generated"

bcrypt.compare(
    incomingPassword,
    existingHash,
    (err, res) => {
        if(res && res === true) {
            return console.log("Valid Password");
        }
        //invalid password handling here
        else {
            console.log("Invalid Password");
        }
});

密碼存儲

無論你是用數據庫還是文件來存儲密碼,都不能使用明文存儲。我們在上一節已經學到,你可以將密碼進行哈希后存儲在數據庫中。我推薦密碼字段用varchar(255)數據類型,你也可以選擇不限長度的字段類型。如果你使用的是bcrypt,則可以使用varchar(60)字段類型,因為bcrypt會生成固定長度為60個字符的哈希。

認證和授權

一個擁有合適的角色權限系統,將會阻止一些惡意用戶在系統中做一些越權的事情。為了實現正確的授權過程,將合適的角色和權限分配給每個用戶,以便他們可以執行權限范圍內的某些任務。在Node.js中,你可以使用著名的ACL模塊,根據系統中的授權來開發訪問控制列表。

const ACL = require('acl2');
const acl = new ACL(new ACL.memoryBackend());
// guest is allowed to view blogs
acl.allow('guest', 'blogs', 'view')
// check if the permission is granted
acl.isAllowed('joed', 'blogs', 'view', (err, res) => {
    if(res){
        console.log("User joed is allowed to view blogs");
    }
});

請查閱acl2文檔以獲取更多信息和示例代碼。

4. 暴力攻擊防護

黑客經常會使用軟件反復使用不同的密碼嘗試獲得系統權限,直到找到有效密碼為止,這種攻擊方式叫做暴力攻擊。為了避免這種攻擊,一種簡單有效的辦法是“讓他等一會”,也就是說,當某人嘗試登錄系統並嘗試輸入無效密碼3次以上時,請讓他們等待60秒左右,然后再嘗試。這樣,攻擊者將大大提高時間成本,並且將使他們永遠無法破解密碼。

防止這種攻擊的另一種方法是屏蔽無效登錄請求的IP。系統在24小時內允許每個IP進行3次錯誤地登錄嘗試。如果有人嘗試進行暴力破解,則將其IP封鎖24小時。 許多公司已使用這種方法來防止暴力攻擊。 如果使用Express框架,則有一個中間件模塊可在傳入請求中啟用速率限制。 它稱為express = brute

下面是一個例子。

安裝依賴項

npm install express-brute --save

在路由中啟用它

const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
const bruteforce = new ExpressBrute(store);

app.post('/auth',
    bruteforce.prevent, // error 429 if we hit this route too often
    function (req, res, next) {
        res.send('Success!');
    }
);
//...

5. HTTPS安全傳輸

現在已經2021年了,你也應該使用HTTPS來向網絡中發送數據了。HTTPS是具有安全通信支持的HTTP協議的擴展。使用HTTPS,可以保證用戶在互聯網中發送的數據是被加密的,是安全的。

在這里我不打算詳細介紹HTTPS協議的工作原理,我們只討論如何使用它。這里我強烈推薦使用LetsEncrypt來為你的所有域名生成安全證書。

你可以在基於Apache和Nginx的Web服務器上使用LetsEncrypt。我強烈建議你在反向代理或網關層上使用HTTPS協議,因為它們有很多繁重的計算操作。

6. 會話劫持保護

會話(session)是任何動態Web應用程序最重要的部分,一個安全的會話對用戶和系統來說真的是非常必要的。會話是使用Cookie實現的,因此必須確保其安全以防止會話劫持。以下是可以為每個cookie設置的屬性列表以及它們的含義:

  • secure - 該屬性告訴瀏覽器僅在通過HTTPS發送請求時才發送cookie。
  • HttpOnly - 該屬性用於防止跨站點腳本攻擊,因為它不允許通過JavaScript訪問cookie。
  • domain - 該屬性用於與請求URL的服務器域名進行比較,如果域名匹配,或者是其子域,那么接下來就會檢查path屬性。
  • path - 除了domian之外,還可以指定cookie有效的URL路徑。 如果domain和路徑匹配,則可以在請求中發送cookie。
  • expires - 該屬性用於設置持久性的cookie,cookie只會在超過了設定的日期才會過期。

在Express框架中,你可以使用express-session npm模塊來管理會話。

const express = require('express');
const session = require('express-session');
const app = express();

app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: true, path: '/'}
}));

7. 跨站點請求偽造攻擊(CSRF)防護

跨站點請求偽造攻擊利用系統中受信任的用戶,對Web應用程序執行有害的惡意操作。在Node.js中,我們可以使用csurf模塊來緩解CSRF攻擊。該模塊需要首先初始化express-sessioncookie-parser,你可以看看下面的示例代碼:

const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const bodyParser = require('body-parser');

// setup route middlewares
const csrfProtection = csrf({ cookie: true });
const parseForm = bodyParser.urlencoded({ extended: false });

// create express app
const app = express();

// we need this because "cookie" is true in csrfProtection
app.use(cookieParser());

app.get('/form', csrfProtection, function(req, res) {
  // pass the csrfToken to the view
  res.render('send', { csrfToken: req.csrfToken() });
});

app.post('/process', parseForm, csrfProtection, function(req, res) {
  res.send('data is being processed');
});

app.listen(3000);

在網頁上,你需要創建一個隱藏輸入域,將CSRF令牌保存在該輸入域中,例如:

<form action="/process" method="POST">
  <input type="hidden" name="_csrf" value="{{csrfToken}}">

  Favorite color: <input type="text" name="favoriteColor">
  <button type="submit">Submit</button>
</form>

如果使用的是AJAX請求,那么CSRF令牌可以通過請求頭(header)來傳遞。

var token = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
  headers: {
    'CSRF-Token': token
  }

8. 拒絕服務

拒絕服務或DOS攻擊,可以讓攻擊者通過破壞系統,使系統被迫關閉服務或用戶無法訪問服務。攻擊者通常會向系統發送大量的流量和請求,從而增加服務器CPU和內存負載,導致系統崩潰。為了緩解Node.js應用程序中的DOS攻擊,首先是要識別此類事件, 我強烈建議將這兩個模塊集成到系統中。

  • Account lockout - 在n次嘗試失敗后,將帳戶或IP地址鎖定一段時間(例如24小時?)
  • Rate limiting - 限制用戶在特定時間段內只能請求系統n次,例如,單個用戶每分鍾只能請求3次。

正則表達式拒絕服務攻擊(ReDOS)是DOS攻擊的一種,攻擊者利用系統中正則表達式的設計缺陷或計算復雜度來大量消耗服務器的系統資源,造成服務器的服務中斷或停止。

我們可以使用一些工具來檢查有風險的正則表達式,從而避免這些正則表達式的使用。例如這個工具:

https://github.com/davisjam/vuln-regex-detector

9. 依賴關系驗證

我們在項目中都使用了大量的依賴項。我們還需要檢查並驗證這些依賴關系,以確保整個項目的安全性。NPM已經具有這樣的審核功能來查找項目的漏洞。只需在源代碼目錄中運行下面的命令即可:

npm audit

要修復漏洞,可以運行此命令:

npm audit fix

您也可以先進行dry run來檢查修復程序,然后再將其應用到項目中。

npm audit fix --dry-run --json

10. HTTP安全頭信息

HTTP提供了一些安全頭信息,可以防止常見的攻擊。如果使用的是Express框架,則可以使用helmet模塊,1行代碼就可以啟用所有安全頭。

npm install helmet --save

下面來看看如何使用:

const express = require("express"); 
const helmet = require("helmet");  
const app = express(); 
app.use(helmet());  
//...

這將啟用以下HTTP頭:

  • Strict-Transport-Security
  • X-frame-Options
  • X-XSS-Protection
  • X-Content-Type-Protection
  • Content-Security-Policy
  • Cache-Control
  • Expect-CT
  • Disable X-Powered-By

這些HTTP頭可防止惡意用戶的各種攻擊,例如點擊劫持,跨站點腳本攻擊等。

(本文完)

公眾號 - 前端新世界


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM