بهترین شیوه‌های امنیتی برای برنامه‌های Nodejs
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 14 دقیقه

بهترین شیوه‌های امنیتی برای برنامه‌های Nodejs

چیزی که توسعه دهندگان حرفه‌ای در پایان چرخه توسعه به آن توجه می‌کنند، بحث امنیت برنامه است. تامین امنیت یک برنامه تجمل گرایی نیست، بلکه یک ضرورت است. شما باید امنیت برنامه خود را در هر مرحله از توسعه مانند معماری، طراحی، کد و در نهایت استقرار در نظر بگیرید. در این آموزش می‌خواهیم روش‌های ایمن سازی برنامه‌ها در nodejs را بیاموزیم. پس با ما همراه باشید.

اعتبار سنجی داده‌ها - هرگز به کاربران خود اعتماد نکنید

شما همیشه باید داده‌های دریافتی از کاربران یا موجودیت‌های دیگر سیستم را بررسی و تایید کنید. اعتبار نامطلوب یا عدم اعتبار سنجی تهدیدی برای سیستم‌عامل است و می‌تواند منجر به سوء استفاده از امنیت شود. بیایید یاد بگیریم که چگونه داده‌های ورودی را در nodejs اعتبار سنجی کنیم. برای انجام اعتبار سنجی داده‌ها می‌توانید از یک ماژول به نام validator استفاده کنید.

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

همچنین برای انجام این کار می‌توانید از ماژولی به نام joi (توصیه شده توسط Codeforgeek) نیز استفاده کنید.

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);
  }

حمله SQL Injection

تزریق 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 قرار داده می‌شود و اکنون کوئری‌ها جدول کاربران را به روزرسانی می‌کند و نام هر کاربر را "John" و نام خانوادگی را "Wick" قرار می‌دهد. این در نهایت منجر به خرابی سیستم شده و اگر پایگاه داده شما بک آپ نداشته باشد، مغلوب خواهید شد.

چگونه می‌توان از حمله تزریق SQL جلوگیری کرد

مفیدترین راه برای جلوگیری از حملات تزریق SQL، پاکسازی داده‌های ورودی است. شما می‌توانید تک تک ورودی‌ها را کنترل کنید یا با استفاده از اتصال پارامتر اعتبار سنجی کنید. اتصال پارامتر بیشتر توسط توسعه دهندگان مورد استفاده قرار می‌گیرد، زیرا کارآیی و امنیت بالایی را ارائه می‌دهد. اگر از ORM معروفی مانند سکیولایز، هایبرنیت و ... استفاده می‌کنید، آنها در حال حاضر توابع را برای تأیید و پاکسازی اطلاعات شما فراهم می‌کنند. اگر از ماژول‌های پایگاه داده غیر از ORM مانند mysql برای Node استفاده می‌کنید، می‌توانید از روش‌های فرار ماژول استفاده کنید. بیایید با مثال یاد بگیریم. پایگاه کد نشان داده شده در زیر از ماژول mysql در نود استفاده می‌کند.

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 استفاده کنید.

Typecasting

جاوااسکریپت یک زبان پویا است، به عنوان مثال یک مقدار می‌تواند از هر نوع داده‌ای باشد. برای تأیید نوع داده‌ها می‌توانید از روش typecasting استفاده کنید تا فقط نوع مقصد مورد نظر وارد پایگاه داده شود. به عنوان مثال شناسه کاربری فقط می‌تواند شماره را بپذیرد، پس باید تایپکست شود تا اطمینان حاصل کنیم که شناسه کاربر فقط باید یک عدد باشد. به عنوان مثال، بیایید به کدی که در بالا نشان داده شده است مراجعه کنیم.

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) استفاده کردیم. برای درک عمیق تایپکستینگ می‌توانید به این مقاله مراجعه کنید.

احراز هویت و تأیید اعتبار

داده‌های حساس مانند گذرواژه‌ها باید به روشی ایمن در سیستم ذخیره شوند تا کاربران مخرب از اطلاعات حساس سوء استفاده نکنند. در این بخش می‌خواهیم بدانیم که چگونه گذرواژه‌هایی را که کاملا عمومی هستند ذخیره و مدیریت کنیم. می‌دانیم که هر برنامه‌ای به نوعی در سیستم خود رمزعبور دارد.

هش کردن رمز عبور

Hashing تابعی است که یک رشته با اندازه ثابت را تولید می‌کند. خروجی تابع hash را نمی‌توان رمزگشایی کرد، از این رو ماهیت آن یک طرفه است. برای داده‌هایی مانند گذرواژه، همیشه باید از الگوریتم‌های درهم سازی (hash) برای تولید یک نسخه هش از رمز عبور استفاده کنید.

شاید از خود بپرسید که اگر هش رشته‌ای یک طرفه است، پس چگونه مهاجمان به رمزهای عبور دسترسی پیدا می‌کنند؟

خوب همانطور که در بالا ذکر کردیم، هش یک رشته ورودی را می‌گیرد و یک خروجی با طول ثابت ایجاد می‌کند. بنابراین مهاجمان رویکردی معکوس دارند و هش‌ها را از لیست رمز عبور عمومی تولید می‌کنند، سپس آنها هش‌های به دست آمده را با هش‌های موجود در سیستم شما مقایسه می‌کنند تا رمز عبور را پیدا کنند. این گونه حملات، حمله جداول جستجو نامیده می‌شوند.

به همین دلیل است که شما به عنوان یک معمار سیستم نباید رمزهای عبور عمومی استفاده شده را در سیستم خود مجاز بگذارید. برای غلبه بر این حمله می‌توانید چیزی به نام "salt" داشته باشید. salt به هش رمز عبور متصل است تا بدون توجه به ورودی، آن را منحصر به فرد کند. salt باید به طور ایمن و تصادفی تولید شود تا قابل پیش بینی نباشد. الگوریتم Hashing که ما به شما پیشنهاد می‌دهیم Bcrypt است. در زمان نگارش این مقاله، Bcrypt مورد بهره برداری قرار نگرفته و از نظر رمزنگاری ایمن در نظر گرفته نشده است. در nodejs می‌توانید از ماژول Bcrypt برای انجام هش استفاده کنید.

لطفا به کد مثال زیر مراجعه بفرمایید.

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 هزینه تابع هش است. هرچه هزینه بالاتر باشد، هش امن‌تری ایجاد می‌شود. شما باید salt را براساس قدرت محاسبه سرور خود تعیین کنید. پس از ایجاد هش برای رمز عبور، رمز وارد شده توسط کاربر با هش ذخیره شده در پایگاه داده مقایسه می‌شود. برای درک بیشتر به کد زیر مراجعه کنید.

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");
        }
});

ذخیره رمز عبور

چه از پایگاه داده استفاده می‌کنید چه از سیستم فایل، همیشه سعی کنید نسخه هش شده گذرواژه را ذخیره کنید، نه متن ساده آن را.

همانطور که در بالا ذکر شد، شما باید رشته هش شده رمز عبور را تولید کنید و آن را در سیستم ذخیره کنید. من معمولا استفاده از نوع داده (255)varchar را برای گذرواژه توصیه می‌کنم. همچنین می‌توانید یک قسمت با طول نامحدود انتخاب کنید. اگر از bcrypt استفاده می‌کنید، می‌توانید از (60)varchar استفاده کنید، زیرا bcrypt هش‌های کاراکتر با اندازه 60 ایجاد می‌کند.

کنترل دسترسی

سیستمی با اجازه کاربری مناسب مانع از این می‌شود که کاربران مخرب خارج از اجازه آن عمل کنند. برای دستیابی به یک پروسه صحیح تعیین مجوز، نقش‌ها و مجوزهای خاص به هر کاربر داده می‌شود تا بتوانند کارهای محدودی را انجام دهند و نه بیشتر. در nodejs می‌توانید از یک ماژول معروف به نام 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 را بررسی کنید.

پیشگیری از حمله Bruteforce

Bruteforce حمله‌ای است که در آن هکر با استفاده از نرم‌افزاری رمزهای عبور مختلف را به صورت تکراری امتحان می‌کند تا زمانی که دسترسی به او داده شود، یعنی رمز عبور معتبری پیدا شود. برای جلوگیری از حمله Bruteforce، یکی از ساده‌ترین راه‌ها این است که کاربر را منتظر نگه دارید. وقتی شخصی در تلاش است تا به سیستم شما وارد شود و بیش از 3 بار رمز عبور نامعتبر را امتحان کرد، قبل از اینکه دوباره امتحان کند، 60 ثانیه باید منتظر بماند. به این ترتیب مهاجم کند خواهد شد و پروسه یافتن رمز به مشکل می‌خورد.

روش دیگر برای جلوگیری از آن ممنوعیت IP است که باعث ایجاد درخواست‌های ورود نامعتبر می‌شود. سیستم شما در هر 24 ساعت 3 بار تلاش اشتباه می‌کند. اگر کسی سعی در اجرای بیش از حد داشته باشد، IP به مدت 24 ساعت مسدود می‌شود. بسیاری از شرکت‌ها از این رویکرد مسدود کردن آی پی برای جلوگیری از حملات Bruteforce استفاده میکنند. اگر از فریمورک 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!');
    }
);
//...

کد از مستندات ماژول express-brute گرفته شده است.

انتقال ایمن با استفاده از HTTPS

سال 2021 است و برای ارسال ایمن داده‌ها و ترافیک خود از طریق اینترنت باید از HTTPS استفاده کنید. HTTPS همان پروتکل HTTP با پشتیبانی از ارتباطی ایمن است. با استفاده از HTTPS می‌توانید اطمینان حاصل کنید که ترافیک و داده‌های کاربر شما از طریق اینترنت رمزگذاری شده و ایمن هستند.

قصد نداریم در اینجا نحوه کار HTTPS را با جزئیات توضیح دهیم. بلکه می‌خواهیم روی قسمت اجرایی آن تمرکز کنیم. همچنین توصیه می‌کنیم که از LetsEncrypt برای تولید گواهینامه‌های SSL برای همه دامنه‌ها / زیردامنه‌های خود استفاده کنید.

این برنامه رایگان است و هر 90 روز یک Daemon برای به روزرسانی گواهینامه‌های SSL اجرا می‌کند. می‌توانید از اینجا درباره LetsEncrypt اطلاعات بیشتری کسب کنید. اگر چندین زیردامنه داشته باشید، می‌توانید یک گواهینامه خاص دامنه یا یک مجوز wildcard انتخاب کنید. LetsEncrypt از هر دو پشتیبانی می‌کند.

به علاوه می‌توانید از LetsEncrypt برای سرورهای وب مبتنی بر Apache و Nginx نیز استفاده کنید. به شدت توصیه می‌کنیم مذاکرات SSL را در پروکسی معکوس یا در لایه gateway انجام دهید، زیرا این یک کار محاسباتی سنگین است.

پیشگیری از Session Hijacking

session بخش مهمی از هر برنامه وب پویا است. داشتن یک سشن ایمن در برنامه برای امنیت کاربران و سیستم‌ها ضروری است. یک سشن با استفاده از کوکی‌ها اجرا می‌شود و برای جلوگیری از ربوده شدن سشن باید آن را ایمن نگه دارید. در زیر لیستی از ویژگی‌هایی که می‌توان برای هر کوکی تنظیم کرد به همراه معنی آنها وجود دارد:

  • secure - این ویژگی به مرورگر میگوید فقط در صورت ارسال درخواست از طریق HTTPS کوکی را ارسال کند.
  • HttpOnly - از این ویژگی برای جلوگیری از حملاتی مانند xss استفاده میشود، زیرا اجازه دسترسی به کوکی از طریق جاوااسکریپت را نمی‌دهد.
  • domain - از این ویژگی برای مقایسه با دامنه سرور که نشانی اینترنتی از آن درخواست شده‌است، استفاده می‌شود. اگر دامنه مطابقت داشته باشد یا یک زیردامنه باشد، در ادامه ویژگی path بررسی می‌شود.
  • path - علاوه بر دامنه، می‌توان مسیر URL را که کوکی برای آن معتبر است مشخص کرد. اگر دامنه و مسیر با هم مطابقت داشته باشند، کوکی با درخواست ارسال می‌شود.
  • expires - این ویژگی برای تنظیم کوکی‌های ثابت مورد استفاده قرار می‌گیرد، زیرا کوکی منقضی نمی‌شود تا زمانی که تاریخ تعیین شده فرا برسد.

برای انجام مدیریت سشن در فریمورک 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: '/'}
}));

در اینجا می‌توانید اطلاعات بیشتری در مورد مدیریت سشن در Express کسب کنید.

جلوگیری از حمله  Cross Site Request Forgery (CSRF)

CSRF حمله‌ای است که در آن کاربر مورد اعتماد سیستم به منظور اجرای اقدامات مخرب، برنامه وب را دستکاری می‌کند.

در nodejs می‌توان از ماژول csurf برای کاهش حمله CSRF استفاده کرد. این ماژول ابتدا به express-session یا cookie-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 را در هدر منتقل کنید.

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

انکار سرویس (Denial of Service)

انکار سرویس یا DOS نوعی حمله است که مهاجمان سعی دارند با تداخل در سیستم، سرور را پایین بیاورند یا آن را برای کاربران غیرقابل دسترسی کنند. مهاجم به طور کلی سرور را با مراجعه بیش از حد به سیستم و درخواست‌های زیادی اشغال می‌کند که به نوبه خود باعث افزایش بار CPU و حافظه می‌شود و در نهایت منجر به خرابی سیستم می‌گردد. برای کاهش حملات DOS در برنامه‌های nodejs، اولین قدم شناسایی چنین رویدادی است. توصیه می‌شود که این دو ماژول در سیستم ادغام شوند.

  1. قفل حساب: بعد از n تعداد تلاش ناموفق، حساب یا آدرس IP را برای مدتی قفل کنید (مثلا 24 ساعت).
  2. محدود کردن نرخ: کاربران را در درخواست از سیستم در یک دوره خاص محدود کنید. به عنوان مثال 3 درخواست در هر دقیقه از یک کاربر خاص.

ReDOS نوعی حمله DOS است که در آن مهاجم از پیاده سازی منظم عبارات در سیستم سوء استفاده می‌کند. برخی از عبارات منظم قدرت محاسباتی زیادی لازم دارند و مهاجم می‌تواند با ارسال درخواست‌هایی که شامل عبارات منظم در سیستم است، از آن سوء استفاده کند که به نوبه خود باعث افزایش بار سیستم می‌شود و در نهایت منجر به خرابی سیستم می‌گردد. شما می توانید از این نرم‌افزار برای تشخیص عبارات منظم و جلوگیری از استفاده از آنها در سیستم خود بهره بگیرید.

همه ما در پروژه‌های خود از وابستگی استفاده می‌کنیم. برای اطمینان از امنیت کل پروژه باید این وابستگی‌ها را نیز بررسی و اعتبارسنجی کنیم. NPM از قبل دارای ویژگی حسابرسی برای یافتن آسیب پذیری پروژه است. فقط کافی است دستور زیر را در دایرکتوری کد منبع خود اجرا کنید.

npm audit

برای رفع آسیب پذیری می‌توانید این دستور را اجرا کنید.

npm audit fix

همچنین می‌توانید آن را به صورت تستی اجرا کرده و قبل از استفاده در پروژه خود اصلاح کنید.

npm audit fix --dry-run --json

هدرهای امنیتی HTTP

HTTP چندین هدر امنیتی ارائه می دهد که می‌تواند از حملات شناخته شده جلوگیری کند. اگر از فریمورک Express استفاده می‌کنید، می‌توانید از ماژولی به نام helmet استفاده کنید تا همه هدرهای امنیتی را با یک خط کد فعال کنید.

دستور زیر نحوه استفاده از آن را نشان می‌دهد:

npm install helmet --save

دستور زیر هم هدرهای HTTP را فعال می‌کند:

const express = require("express"); const helmet = require("helmet");  const app = express(); app.use(helmet());  //...
  • Strict-Transport-Security
  • X-frame-Options
  • X-XSS-Protection
  • X-Content-Type-Protection
  • Content-Security-Policy
  • Cache-Control
  • Expect-CT
  • Disable X-Powered-By

این هدرها از انواع مختلف حملات مانند clickjacking، cross-site scripting و غیره در برابر کاربران مخرب جلوگیری می‌کنند.

جمع‌بندی

یک برنامه امن nodejs از داده‌ها و اطلاعات کاربران محافظت می‌کند و اعتبار برنامه را افزایش می‌دهد. این توصیه‌ها براساس سال‌ها تجربه کار در اکوسیستم nodejs تدوین شده است. اگر هر موردی را فراموش کرده‌ایم، لطفا در بخش زیر با ما در میان بگذارید تا آن را به لیست اضافه کنیم.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
5 از 1 رای

/@heshmati74
عرفان حشمتی
Full-Stack Web Developer

کارشناس معماری سیستم های کامپیوتری، طراح و توسعه دهنده وب سایت

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

عرفان حشمتی

Full-Stack Web Developer