چگونه از SQL Injection جلوگیری کنیم؟

از سری مطالب دوره آموزشی کد نویسی امن در وب این قسمت به سراغ injection از نوع sql injection می رویم.برای جلوگیری از هرگونه آسیب پذیری ابتدا باید آسیب پذیری را متوجه شد پس ابتدا مروری بر ساز و کار های این آسیب پذیری خواهیم داشت و سپس چند نمونه کد آسیب پذیر در php و asp.net و java بررسی می کنیم.در انتها چند روش برای جلوگیری از این حمله را در کد بررسی می کنیم.زبان پرس و جو sql راهی برای مدیریت داده های ذخیره شده در پایگاه داده های رابطه ای است.به این معنا که می توانیم در هر بستری از جمله وب داده هارا ذخیره، ویرایش و یا تغییر دهیم.گاهی اوقات احراز هویت کاربران، نمایش محتوای وبسایت و یا عکس های کاربران در پایگاه داده ذخیره می شود و توسط sql در مسیر های مختلف به کار برده می شود.حال فرض کنید attacker در پرس و جوی sql، دستکاری و یا Inject کند.در اینجا می گوییم sql injection رخ داده است.برای مثال در تصویر بالا attacker به جای مقدار دسته بندی محصولات(Gifts) این payload را وارد می کند.' UNION SELECT username, password FROM users--در نتیحه علاوه بر اطلاعات محصول دسته بندی Gifts تمام username و password های جدول users را هم در خروجی نمایش می دهد.حملات sql injection نوع های مختلفی دارد. چون به ازای هر query نوشته شده ممکن است حمله جدیدی رخ دهد.با این حال به صورت کلی 3 دسته بندی زیر را می توان نام برد.1-بر مبنای خطا یا Error Basedمثال معروفی برای این روش وجود دارد.صفحه ای از وبسایتی برای نمایش جزییات خبر از id آن خبر استفاده می کند؛https://web.tld/news?id=100با توجه به مقدار id، داده های دیگری از پایگاه داده خوانده و نمایش داده می شود.SELECT title,context FROM tbl_news WHERE id ='$id'2-بر مبنای اجتماع یا Union Basedدر این روش علاوه بر اطلاعات دریافت شده توسط پرس و جوی توسعه دهنده با دستور Union اطلاعات دیگری از پایگاه داده استخراج می شود.http://fakesite.com/report.php?id=23برای مثال فرض کنید صفحه ای وجود دارد که براساس id موجودیت های سیستم گزارشی از وضیعت آن موجودیت چاپ می کند.اگر مقدار id را به همراه دستور زیر وارد کنیم، می توانیم تعداد ستون های جدولی که با توجه به id، اطلاعات از آن خوانده می شود را بدست آوریم.(ممکن است مقدار 5 به ازای هر جدول متفاوت باشد)http://fakesite.com/report.php?id=23 order by 5--+بعد از دانستن تعداد ستون های جدول با دستور Union، ستونی که از آن اطلاعات خوانده می شود و در صفحه چاپ می شود را بدست می آوریم.http://fakesite.com/report.php?id=-23 union select 'hello1','hello2','hello3','hello4','hello5'--+هر کدام از hello ها که در صفحه چاپ شود به این معنی است که می توانیم به جای آن hello پرس و جوی خود را بنویسیم.برای مثال نام دیتابیس را با database() بدست می آوریم.http://fakesite.com/report.php?id=-23 union select 1,2,database(),4,5--+3-بر مبنای تاریکی Blind Basedشاید اسم این روش عجیب باشد ولی با توجه به الگو "بر مبنای" تاریکی کلمه مناسب و هکر پسندانه تری است!در این روش با استفاده از and و دستورات زمانی مانند sleep در بارگذاری اطلاعات از پایگاه داده تاخییر و یا تغییر ایجاد می کنیم.برای مثال می دانیم 2=1 نیست پس آن را در مقدار id قرار می دهیم تا شرط False شود و بدین ترتیب اطلاعات به درستی از پایگاه داده استخراج نشود.http://www.shop.local/item.php?id=34 and 1=2در مرحله بعد شرط True می کنیم.http://www.shop.local/item.php?id=34 and 1=1حال با توجه به روش های بالا به سراغ روش های جلوگیری از این حمله می رویم.روش های جلوگیری در زبان PHPمثال 1)تکه کد زیر دو مقدار username و password را با درخواست از نوع POST دریافت می کند و سپس در پرس و جو با رکورد های موجود در پایگاه داده مقایسه می کند.<?php $userName=$_POST['userName']; $password=$_POST['password']; $sqlQuery=&quotSELECT * FROM users WHERE user_name='&quot.$username.&quot' AND user_password='&quot.$password&quot';&quot ?>اگر password، مقدار ‘ or ‘a’=’a ‘or’ داشته باشد؛ شرط WHERE به ازای کاربر با کلمه عبور نامرتبط درست خواهد بود.مثال 2)اطلاعات کاربر را با توجه به id او توسط پرس و جو sql دریافت می کنیم.$expected_data = 1; $query = &quotSELECT * FROM users where id=$expected_data&quotاگر id، مقدار1; DROP TABLE users; داشته باشد علاوه بر نمایش اطلاعات کاربر جدول کاربران هم پاک می شود!و پرس و جو در نهایت این شکلی می شود.SELECT * FROM users where id=1; DROP TABLE users;برای جلوگیری دو روش Block Malicious و Prepared Statements را بررسی می کنیم.راه حل 1)یکی از بهترین روش ها برای جلوگیری از آسیب پذیری، تشخص کاراکتر های غیر مجاز درون رشته و تبدیل آن ها به کاراکتر های مجاز است.با این توضیح ابتدا تابعی تعریف می کنیم تا ورودی کاربر را بگیرد و کاراکتر های برهم زننده رشته پرس و جو را به کاراکتر های مجاز در رشته در قالب آرایه تبدیل کند.<?php function BlockSQLInjection($str) { return str_replace(array(&quot'&quot,&quot&quot&quot,&quot'&quot,'&quot'),array(&quot'&quot,&quot&quot&quot'&quot,&quot&quot&quot,$str)); } ?>حال درخواست های ارسال شده را به تابع پاس دهیم.<?php $userName=BlockSQLInjection($_POST['userName']); $password=BlockSQLInjection($_POST['password']); ?>راه حل 2)یکی از روش های بسیار خوب استفاده از Prepared Statements ها در زبان های مختلف است.برای مثال در زبان php می توان از PDO برای کار با قسمت های مختلف پایگاه داده در قالب api استفاده کرد.نمونه کد برای کار pdo به شکل زیر است.<?php $stmt=$conn->prepare(INSERT INTO MyGuests(firstname,lastname,email)VALUES(?,?,?)&quot); $stmt->bind_param(&quotsss&quot,$firstname,$lastname,$email); //set paramters and execute $firstname=&quotJohn&quot $lastname=&quotDoe&quot $email=&quotjohn@example.com&quot $stmt->execute(); $firstname=&quotMary&quot $lastname=&quotMoe&quot $email=&quotmary@example.com&quot $stmt->execute();روش های جلوگیری در ASP.NETمثال 1)کد زیر را در نظر بگیرید.protected void Button1_Click(object sender, EventArgs e) { string connect = &quotMyConnString&quot string query = &quotSelect Count(*) From Users Where Username = '&quot + UserName.Text + &quot' And Password = '&quot + Password.Text + &quot'&quot int result = 0; using (var conn = new SqlConnection(connect)) { using (var cmd = new SqlCommand(query, conn)) { conn.Open(); result = (int)cmd.ExecuteScalar(); } } if (result > 0) { Response.Redirect(&quotLoggedIn.aspx&quot); } else { Literal1.Text = &quotInvalid credentials&quot }همانطور که در رشته پرس و جو مشاهده می کنید، هیچ کنترلی بر روی درخواست ارسال شده نمی شود.اگر مقدار ' or '1' = '1 ارسال شود شرط WHERE درست می شود.مثال 2)در کد زیر هم درخواست ارسال شده بدون هیچ کنترلی در رشته پرس و جو استفاده شده است.string connect = &quotMyConnString&quot string query = &quotSelect * From Products Where ProductID = &quot + Request[&quotID&quot]; using (var conn = new SqlConnection(connect)) { using (var cmd = new SqlCommand(query, conn)) { conn.Open(); //Process results } }بنابر این اگر مقدار ;Drop Table Admin-- ارسال شود علاوه بر پرس و جوی قبل پرس و جوی ارسال شده اجرا می شود.برای جلوگیری دو زیر را بررسی می کنم.راه حل 1)با توجه به نوع ستون جدول در پایگاه داده، ورودی کاربر را تبدیل به آن نوع می کنم.برای مثال id محصول از نوع عدد و int است پس درخواست ارسالی را تبدیل به عدد می کنم.protected void Page_Load(object sender, EventArgs e) { var connect = ConfigurationManager.ConnectionStrings[&quotNorthWind&quot].ToString(); var query = &quotSelect * From Products Where ProductID = @ProductID&quot using (var conn = new SqlConnection(connect)) { using (var cmd = new SqlCommand(query, conn)) { cmd.Parameters.Add(&quot@ProductID&quot, SqlDbType.Int); cmd.Parameters[&quot@ProductID&quot].Value = Convert.ToInt32(Request[&quotProductID&quot]); conn.Open(); //Process results } } }این روش را می توان به صورت زیر هم استفاده کرد.protected void Page_Load(object sender, EventArgs e) { var connect = ConfigurationManager.ConnectionStrings[&quotNorthWind&quot].ToString(); var query = &quotSelect * From Products Where ProductID = @ProductID&quot using (var conn = new SqlConnection(connect)) { using (var cmd = new SqlCommand(query, conn)) { cmd.Parameters.AddWithValue(&quot@ProductID&quot, Convert.ToInt32(Request[&quotProductID&quot]); conn.Open(); //Process results } } }راه حل 2)روش دیگر استفاده از Store Procedure است.مزیت این روش نسبت به روش قبل این است که query حتما با توجه به نوع ستون اجرا می شود.var connect = ConfigurationManager.ConnectionStrings[&quotNorthWind&quot].ToString(); var query = &quotGetProductByID&quot using (var conn = new SqlConnection(connect)) { using (var cmd = new SqlCommand(query, conn)) { cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(&quot@ProductID&quot, SqlDbType.Int).Value = Convert.ToInt32(Request[&quotProductID&quot]); conn.Open(); //Process results } }روش های جلوگیری در زبان Javaمثال 1)protected void processRequest(HttpServletRequest request, HttpServletResponse response)throws ServletException, IOException { response.setContentType('text/html;charset=UTF-8'); PrintWriter out = response.getWriter(); try { String user = request.getParameter('user'); Connection conn = null; String url = 'jdbc:mysql://192.168.2.128:3306/'; String dbName = 'anvayaV2'; String driver = 'com.mysql.jdbc.Driver'; String userName = 'root'; String password = ''; try { Class.forName(driver).newInstance(); conn = DriverManager.getConnection(url + dbName, userName, password); Statement st = conn.createStatement(); String query = 'SELECT * FROM User where userId='' + user + '''; out.println('Query : ' + query); System.out.printf(query); ResultSet res = st.executeQuery(query); out.println('Results'); while (res.next()) { String s = res.getString('username'); out.println('\t\t' + s); } conn.close(); } catch (Exception e) { e.printStackTrace(); } } finally { out.close(); }با توجه به کد بالا اگر در userId مقدار admin’ or ‘1’=’1 وارد کنیم رشته پرس و جو به شکل زیر می شود.SELECT * FROM User where userId ='admin' or '1'='1'راه حل 1)با استفاده Prepared Statement نوع درخواست ارسال شده را تعیین می کنیم.PreparedStatement preparedStatement=conn.prepareStatement('SELECT * FROM usercheck where username=?') ; preparedStatement.setString(1, user);راه حل 2)با استفاده از NamedQuery در جاوا می توانیم درخواست را به صورت Prepared Statement دریافت و پرس و جو مرتبط ایجاد کنیم.در حالت عادی query به شکل زیر ساخته می شود.String q='SELECT r FROM User r where r.userId=''+user+'''; Query query=em.createQuery(q); List users=query.getResultList();اما با NamedQury می توانیم پرس و جو را با توجه به مدل های پایگاه داده خودکار تولید کنیم.Query query=em.createNamedQuery('User.findByUserId'); query.setParameter('userId', user); List users=query.getResultList();نسخه انگلیسی و کامل تر این مطالب در اینجا وجود دارد. نمونه Source Code با PHP نمونه Source Code با ASP.NET نمونه Source Code با Javaمنابعmemoryleaks.irsecurecoding.irbawetech.comمعذرت بابت کم و کاستی های فراوان این مطلبخیلی خوشحال میشوم نظرات خود را اینجا بگذاریدبا تشکر از شما