oruji.github.io
oruji.github.ioPersian Tutorials
ویرایش: 1396/11/22 22:34
A A

آموزش امنیت در جنگو (Django)

اینترنت می تواند یک مکان ترسناک باشد.

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

به عنوان توسعه دهندگان وب، ما وظیفه داریم راه هایی را که می توان با این نیروهای تاریکی مبارزه کرد را بیان کنیم. هر توسعه دهنده ی وب نیاز دارد با امنیت به عنوان یک جنبه اساسی از برنامه نویسی وب رفتار کند. متاسفانه، پیاده سازی امنیت سخت است – مهاجمان نیاز است تنها یک آسیب پذیری پیدا کنند، ولی مدافعان باید همه ی این آسیب پذیری ها را محافظت کنند.

جنگو تلاش می کند این سختی را سبک تر کند. جنگو جهت محافظت شما به طور خودکار از بسیاری از این اشتباهات امنیتی طراحی شده است که توسعه دهندگان جدید (و حتی با تجربه ها) ایجاد می کنند. ولی همچنان اطلاع کسب کردن از ماهیت این مشکلات، نحوه ی محافظت شما توسط جنگو با اهمیت می باشد، و – از همه مهم تر – مراحلی است که می توان برای امنیت بیشتر کد خود انجام داد.

قبل از هر چیز، یک سلب مسئولیت مهم: ما قصد ارائه ی یک راهنمای قطعی برای هر امنیت وب را نداریم، و همچنین تلاشی سعی نداریم هر آسیب پذیری را در یک روش جامع توضیح دهیم. در عوض، یک خلاصه ی کوتاه از مشکلات امنیتی را که در جنگو بکار می روند را ارائه خواهیم کرد.

موضوع امنیت (Security) وب

اگر قرار است تنها یک چیز از این آموزش از کتاب یاد بگیرید، اجازه دهید آن این باشد:

هرگز – تحت هیچ شرایطی – به داده های از سمت مرورگر اعتماد نکنید.

هرگز کسی را که آن طرف اتصال HTTP می باشد را نمی شناسید. ممکن است یکی از کاربران شما باشد، ولی به سادگی می تواند یک cracker نابکار بدنبال یک موقعیت باشد.

هر داده ای از هر نوعی که از سمت مرورگر می آید، باید با آن به چشم یک داده ی مخرب نگاه کرد. این شامل هر دوی داده های "in band" (مانند، داده های ارسالی از فرم ها وب) و "out of band" (مانند، HTTP header ها، کوکی ها، و اطلاعات دیگر درخواست) می باشند. این ها مسائل جزئی ای درخواست می باشند که مرورگر های معمولا به طور خودکار اضافه می کنند.

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

SQL Injection

SQL injection یک سو استفاده ی رایج می باشد که در آن مهاجم پارامترهای صفحه ی وب (از قبیل داده ی GET/POST یا URL ها) را جهت درج تکه کد های دلخواه SQL که یک برنامه وب ساده به طور مستقیم در پایگاه داده ی خود اجرا می کند را تغییر می دهد. این احتمال دارد خطرناک ترین حالت آسیب پذیری باشد – و متاسفانه، یکی از رایج ترین آن می باشد.

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

def user_contacts(request): user = request.GET['username'] sql = "SELECT * FROM user_contacts WHERE username = '%s';" % username # execute the SQL here...

هر چند، در ابتدا این خطرناک به نظر نمی آمد، ولی واقعا خطرناک است.

ابتدا، تلاش ما در حفاظت از کل لیست پست الکترونیک با یک کوئری به طور هوشمندانه ساخته شده شکست خواهد خورد. فرض کنید، اگر یک مهاجم در box کوئری تایپ کند "' OR 'a'='a" چه اتفاقی می افتد. در این مورد، کوئری که رشته ی مورد نظر درون آن قرار داده شود بدین گونه خواهد بود:

SELECT * FROM user_contacts WHERE username = '' OR 'a' = 'a';

به این دلیل که به SQL نا امن را درون رشته اجازه دادیم، عبارت OR مهاجم مطمئن است که هر ردیف تنها برگردانده شده است.

هر چند، که حداقل حمله ترسناک می باشد. تصور کنید، اگر مهاجم عبارتی شبیه به این را ارسال کنید چه اتفاقی خواهد افتاد "'; DELETE FROM user_contacts WHERE 'a' = 'a". کوئری کامل چیزی شبیه به این خواهد بود:

SELECT * FROM user_contacts WHERE username = ''; DELETE FROM user_contacts WHERE 'a' = 'a';

اوه! تمام لیست تمام به سرعت حذف می شود.

راهکار

اگرچه این مشکل موذیانه و گاهی اوقات کشف آن سخت می باشد، راهکار آن ساده می باشد: هرکز به داده ی ارسال شده از سمت کاربر اعتماد نکنید، و همواره از ارسال آن به SQL اجتناب کنید.

API پایگاه داده ی جنگو این کار را برای شما انجام می دهد. این API به طور خودکار تمام پارامترهای ویژه ی SQL را رد می کند، با توجه به قراردادهای به نقل از سرور پایگاه داده ای که از آن استفاده می کنید (مانند PostgreSQL یا MySQL).

برای مثال، در این فراخوانی API:

foo.get_list(bar__exact="' OR 1=1")

جنگو بر این اساس از ورودی را رد می کند، نتیجه چیزی شبیه به این خواهد بود:

SELECT * FROM foos WHERE bar = '\' OR 1=1'

کاملا بی ضرر.

این در تمام API پایگاه داده ی جنگو بکار برده می شود، با تعدادی استثنا:

در هر یک از این موارد، محافظت از خودتان ساده می باشد. در هر مورد، از رشته ی interpolation دوری کنید وقتی می توانید از ارسال پارامترهای bind استفاده کنید. مثالی که در این بخش شروع کردیم باید به صورت زیر باشد:

from django.db import connection def user_contacts(request): user = request.GET['username'] sql = "SELECT * FROM user_contacts WHERE username = %s" cursor = connection.cursor() cursor.execute(sql, [user]) # ... do something with the results

متد سطح پایین execute یک رشته ی SQL با placeholder یعنی %s دریافت می کند و به طور خودکار پارامترهایی را که از لیست ارسال شده به صورت آرگومان دوم را رد و درج می کند. می توان همواره با این روش SQL سفارشی ساخت.

متاسفانه، نمی توان از پارامترهای bind در هر جایی از SQL استفاده کرد؛ آن ها به صورت identifier ها مجاز نمی باشند (مانند، جدول یا نام های ستون). در نتیجه، در صورت نیاز، تصور کنید، به صورت پویا یک لیست از جداول از یک متغیر POST ساخته شده است، نیاز خواهید داشت آن نام را در کد کد escape کنید. جنگو یک تابع با نام django.db.connection.ops.quote_name ارائه می دهد، که identifier را بر طبق الگوی quoting پایگاه داده ی فعلی escape می کند.

اسکریپت نویسی Cross-Site (XSS)

اسکریپت نویسی cross-site (XSS)، کشف شده در برنامه های وب می باشد که جهت escape کردن محتوای ارسالی کاربر قبل از render کردن آن درون HTML می باشد. این موضوع به مهاجم اجازه می دهد HTML دلخواه خود را درون صفحه ی وب شما درج کند، معمولا در شکل تگ های <script>.

مهاجمان اغلب از حملات XSS جهت دزدیدن اطلاعات session و کوکی استفاده می کنند، یا جهت فریب کاربران در دادن اطلاعات شخصی به یک شخص غلط (که نام دیگر آن phishing می باشد).

این نوع حمله می تواند یک تعداد از فرم های مختلف دریافت کرده و اغلب دارای تغییر اساسی نامحدود می باشد، بنابراین تنها یک مثال نمونه را بررسی نگاه می کنیم. به view بی نهایت ساده ی "Hello, World" ملاحظه کنید:

from django.http import HttpResponse def say_hello(request): name = request.GET.get('name', 'world') return HttpResponse('<h1>Hello, %s!</h1>' % name)

این view به سادگی نام پارامتر GET را خوانده و آن نام را به HTML تولید شده ارسال می کند. بنابراین، در صورتی که به http://example.com/hello/?name=Jacob دسترسی پیدا کنیم، صفحه حاوی کد زیر خواهد بود:

<h1>Hello, Jacob!</h1>

اما صبر کنید – چه اتفاقی می افتد اگر به http://example.com/hello/?name=Jacob دسترسی پیدا کنید؟ در اینصورت کد زیر اتفاق می افتد:

<h1>Hello, <i>Jacob</i>!</h1>

البته، یک مهاجم از چیز ابتدایی مانند تگ های <i> استفاده نمی کند؛ وی یک مجموعه ی کامل از HTML که صفحه ی شما را به محتوای دلخواه سرقت کند در آنجا قرار می دهد. این نوع حمله جهت فریب دادن کاربر برای وارد کردن داده به وب سایت هایی مانند بانک و غیره استفاده می شود، ولی در واقع یک XSS-hijacked، اطلاعات برگشتی حساب را به یک مهاجم ارسال می کند.

مشکل وخیم تر می شود اگر این داده را درون پایگاه داده ذخیره کنید و بعد آن را درون سایت خود نمایش دهید. برای مثال، MySpace یک بار برای یک حمله ی XSS از این نوع آسیب پذیر بوده است. یک کاربری که جاوا اسکریپت درون پروفایل شما درج کرده است که به طور خودکار او را به صورت دوست شما اضافه می کند زمانی که صفحه پروفایل او را بازدید می کنید. در عرض چند روز، او میلیون ها دوست خواهد داشت.

حالا، این ممکن است نسبتا ملایم باشد، ولی به خاطر داشته باشید که این مهاجمان برای بدست آوردن کد خود مدیریت می کنند – نه MySpace – اجرا کردن در کامپیوتر شما. این تجاوزات مورد اعتماد فرض شده اند که تمام کد در MySpace واقعا توسط MySpace نوشته شده است.

MySpace به شدت شانس آورد که این مخربان کد به طور خودکار حساب های کاربران را حذف، رمز عبور آن ها را تغییر، سایت را با spam غرق، یا هر سناریو خوفناک دیگری که احتمال داشت را انجام ندادند.

راهکار

راهکار ساده است: همواره از هر محتوایی را که ممکن است از سمت یک کاربر قبل از درج آن درون HTML باشد دوری کنید.

جهت محافظت در برابر این، سیستم template جنگو به طور خودکار تمام مقادیر متغیر را escape می کند. اجازه دهید ببینیم چه اتفاقی می افتد اگر مثال خودمان را با استفاده از سیستم template بنویسیم:

# views.py from django.shortcuts import render_to_response def say_hello(request): name = request.GET.get('name', 'world') return render_to_response('hello.html', {'name': name}) # hello.html <h1>Hello, {{ name }}!</h1>

با استفاده از این سیستم، یک درخواست به http://example.com/hello/name=Jacob نتیجه اش کد زیر خواهد بود:

<h1>Hello, <i>Jacob</i>!</h1>

auto-escaping جنگو در آموزش template جنگو مورد پوشش قرار داده شده است، همراه با راه های خاموش کردن آن. ولی حتی اگر از این خصوصیت استفاده کنید، باید همچنان این عادت را داشته باشید که همواره از خود سوال را کنید "این داده از کجا آمده است؟" هیچ راهکار خودکاری هرگز سایت شما را از حملات XSS به طور 100% حفاظت نمی کند.

درخواست ساختگی Cross-Site

درخواست ساختگی cross-site (cross-site request forgery، CSRF)، هنگامی که یک مخرب کاربران را در بارگذاری ندانسته یک URL از یک سایت که آنها قبلا در آن authenticate شدن اتفاق می افتد – از این رو از وضعیت authenticate شده آن ها سود می برند.

جنگو (Django) دارای ابزار داخلی برای محافظت از این قبلی حملات می باشد. هر دوی خود حمله و ابزار مقابله با آن به تفصیل در پکیج django.contrib توضیح داده شده است.

ربودن/جعل کردن Session

این یک حمله ی خاص نیست، بلکه یک کلاس عمومی از حملات در یک داده session کاربر می باشد. می تواند به شکل های مختلف باشد:

راهکار

تعدادی اصول کلی وجود دارد که می تواند شما را از این حملات محافظت کنند:

توجه کنید که هیچ از آن اصول و ابزارها حملات man-in-the-middle را جلوگیری نمی کنند. شناسایی این نوع از حملات تقریبا غیر ممکن است. در صورتی که سایت شما اجازه می دهد کاربران برای مشاهده ی هر گونه از داده های حساس وارد شوند، باید همواره آن سایت درون HTTPS خدمات دهد. علاوه بر این، در صورتی که دارای یک سایت SSL-enabled هستید، باید تنظیم SESSION_COOKIE_SECURE را مقدار True قرار دهید؛ این باعث می شود جنگو کوکی های session را از درون HTTPS ارسال کند.

E-mail Header Injection

SQL injection نسبت به برادر خود e-mail header injection کم تر معروف است، که فرم های وب ارسال شده در پست الکترونیکی را سرقت می کند. یک مهاجم می تواند از این تکنیک برای ارسال spam از طریق mail سرور ما استفاده کند. هر فرمی که هدرهای ایمیل را از داده فرم وب می سازد برای این نوع حمله آسیب پذیر می باشد.

اجازه دهید نگاهی به تماس canonical که در بسیاری از سایت ها پیدا شده است بیاندازیم. معمولا این یک پیام به صورت کد مستقیم به یک آدرس ایمیل ارسال می کند و از این رو، در نگاه اول برای سو استفاده ی spam آسیب پذیر ظاهر نمی شود.

هرچند، اغلب این فرم ها همچنین به کاربر برای تایپ در subject خودش برای ایمیل اجازه می دهند (به همراه یک آدرس "from"، بدنه، و گاهی اوقات فیلدهای دیگر). این فیلد subject برای ساخت هدر "subject" از پیام ایمیل استفاده می شوند.

در صورتی که آن هدر هنگام ساختن پیام ایمیل escape نشده باشد، یک مهاجم می تواند چیزی مانند "hello\ncc:spamvictim@example.com" ارسال کند (جایی که "\n" یک کارکتر خط جدید می باشد). آن ایمیل ساخته شده را به شکل زیر تغیری می دهد:

To: hardcoded@example.com Subject: hello cc: spamvictim@example.com

همانند SQL injection، در صورتی که به خط subject داده شده توسط کاربر اعتماد کنیم، اجازه خواهیم داد به او که یک مجموعه از هدرهای مخرب را بسازد، و او می تواند از contact خود برای ارسال spam استفاده کند.

راهکار

می توان با روش همسانی که برای جلوگیری از SQL injection استفاده می کردیم برای جلوگیری از این حمله استفاده کرد: همواره محتوای ارسال شده توسط کاربر را escape یا validate کنید.

توابع mail داخلی جنگو (در django.core.mail) به سادگی به خط های جدید در هر فیلد اجازه نمی دهد برای ساختن هدر استفاده شوند (from و address، به اضافه ی subject). در صورتی که سعی به استفاده از django.core.mail.send_mail با یک subject دارید که حاوی خط های جدید می باشد، جنگو یک خطای BadHeaderError ایجاد می کند.

در صورتی که از توابع داخلی mail جهت ارسال ایمیل استفاده نمی کند، نیاز خواهید داشت، اطمینان حاصل کنید که خط های جدید در هدرها موجب بروز خطا شده یا حذف شوند. ممکن است بخواهید کلاس SafeMIMETest را در django.core.mail برای مشاهده ی انجام این عمل توسط جنگو را بررسی کنید.

پیمایش دایرکتوری

پیمایش دایرکتوری یکی دیگر از حملات به شکل injection می باشد، جایی که یک کاربر مخرب کد filesystem را برای خواند و/یا نوشتن فایل هایی که وب سرور نباید به آن ها دسترسی داشته باشد فریب می دهند.

یک مثال ممکن یک view باشد که فایل ها را بدون دقت نام آن فایل می خواند:

def dump_file(request): filename = request.GET["filename"] filename = os.path.join(BASE_PATH, filename) content = open(filename).read() # ...

هر چند به نظر می رسد که view دسترسی فایل را به فایل های زیر BASE_PATH (توسط استفاده از os.path.join) محدود کرده است، در صورتی که مهاجم در یک filename محتویات .. (مختصر نویسی دایکرتوری پدر) را ارسال کند، می تواند به فایل های بالای BASE_PATH دسترسی پیدا کند. در دست ترجمه ....

هر چیزی که فایل ها را بدون escape مناسب بخواند برای این مشکل آسیب پذیر است. view هایی که فایل ها را به این شکل آسیب پذیر می نویسند، دارای عواقب دو چندان وخیم می باشند.

تغییر دیگر از این مشکل در کدی قرار دارد که به طور پویا مازول های بر اساس URL یا اطلاعات درخواست دیگر را بارگذاری می کند. یک مثال شایع در مورد دنیای Ruby on Rails می باشد. قبل از اواسط سال 2006، Rails از URL های شبیه به http://example.com/person/poke/1 به طور مستقیم برای بارگذاری ماژول های و فراخوانی متدها استفاده می کرد. نتیجه آن بود که یک URL به دقت ساخته شده می توانست به طور خودکار کد دلخواه را بارگذاری کنید، شامل یک اسکریپت دوباره راه اندازی کننده ی دیتابیس!

راهکار

در صورتی که کد شما هموراه نیاز به خواندن یا نوشتن فایل ها بر اساس ورودی کاربر دارد، نیاز به تمیز کردن مسیر درخواست شده به طور بسیار با دقت جهت اطمینان از این که یک مهاجم قادر به escape از دایرکتوری پایه ای که شما دسترسی به آن را محدود کرده اید دارید.

یک مثال خوب از نحوه ی انجام این escape در محتوی ارائه شده ی استاتیک view می باشد (در django.views.static). در اینجا کد مرتبط وجود دارد:

import os import posixpath # ... path = posixpath.normpath(urllib.unquote(path)) newpath = '' for part in path.split('/'): if not part: # strip empty path components continue drive, part = os.path.splitdrive(part) head, part = os.path.split(part) if part in (os.curdir, os.pardir): # strip '.' and '..' in path continue newpath = os.path.join(newpath, part).replace('\\', '/')

جنگو فایل ها را نمی خواند (مگر این که از تابع static.serve استفاده کنید، ولی آن با کدی که نشان داده شد محافظت شده است)، بنابراین این آسیب پذیری بر هسته ی کد ندارد.

علاوه بر این، استفاده از URLconf abstraction بدین معناست که جنگو هرگز کدی را که به طور واضح نگفته اید بارگذاری شود بارگذاری نمی کند. هیچ راهی برای ساختن یک URL ای که موجب می شود جنگو چیزی را که در یک URLconf ذکر نشده است را بارگذاری کند وجود ندارد.

پیام های خطای محافظت نشده

در طول توسعه، قادر بودن برای مشاهده ی traceback ها و خطا هایی که در مرورگر شما وجود دارند به شدت مفید می باشد. فریم ورک یا چارچوب جنگو (Django) دارای پیام های debug خوب و آموزنده ای جهت debug کردن آسان تر می باشد.

البته در صورتی که، این خطا ها یک بار در سایتی نمایش داده شود، می تواند جنبه هایی از کد شما یا پیکربندی را فاش کند که می تواند به یک مهاجم کمک کند.

بعلاوه، خطا ها و traceback ها همیشه برای کاربران مفید نمی باشد. فلسفه ی جنگو این است که بازدید کنندگان سایت نباید هرگز پیام های خطای مرتبط به برنامه را مشاهده کنند. در صورتی که کد شما یک خطای handle نشده را ایجاد کند، یک بازدید کننده ی سایت نباید traceback کامل را مشاهده کند – یا هر اشاره ای از تکه های کد یا پیام های خطای پایتون. در عوض، بازدید کننده باید یک پیام دوستانه "این صفحه قابل دسترسی نمی باشد" دریافت کند.

البته به طور طبیعی، توسعه دهندگان نیاز دارند traceback ها برای debug برنامه ها در کد آن ها مشاهده کنند. بنابراین فریم ورک یا چارچوب باید تمام پیام های خطا را برای عموم مخفی کند، ولی باید آن ها را برای توسعه دهندگان مورد اعتماد سایت نمایش دهد.

راهکار

تنظیم DEBUG جنگو (Django) نمایش این پیام های خطا را کنترل می کند. اطمینان حاصل کنید که این تنظیم زمانی که برای deploy آماده می باشید False است.

کاربرانی که می خواهند deploy را در Apache و mod_python انجام دهند، همچنین باید اطمینان حاصل کنند که دارای PythonDebug Off در فایل های conf آپاچی می باشند؛ این موضوع هر خطایی که قبل از آنکه جنگو دارای یک شانس برای بارگذاری باشد را سرکوب خواهد کرد.

حرف آخر درباره ی امنیت

ما امیدواریم تمام این صحبت های درباره ی مشکلات امنیتی رعب آور نبوده باشد. درست است که وب می تواند یک دنیای وحشی باشد، ولی با کمی دور اندیشی، می توانید یک وب سایت امن داشته باشید.

به خاطر داشته باشید که امنیت وب یک تغییر فیلد به طور دائم است: در صورتی که در حال خواندن نسخه ی dead-tree این کتاب می باشید، مطمئن شوید که بررسی های به روز تری از منابع امنیتی برای هر آسیب پذیری جدید که کشف شده است انجام داده اید. در واقع، همواره ایده ی خوبی است که مدت زمانی را در هفته یا ماه برای جستجو و تحقیق در مورد امنیت برنامه ی وب کنار بگذارید. این یک سرمایه گذاری کوچک است، ولی حفاظتی که برای سایت خود و کاربرانتان بدست می آورید بدون خرج خواهد بود.