网络安全

    一旦你的网站要放到网际网络上,你就得接受被骇客攻击的风险,小则倒站,大则使用者资料被窃取。而从网络设备、作业系统、网站服务器、数据库到应用程式,有高达75%的攻击主要都发生在网站应用程式这一层,因此身为网站开发者的你,对于网络安全不能没有基本的认识。

    所幸Rails本身就内建了许多安全机制,像是SQL injectionXSSCSRF等,可以帮助我们防范常见的数种网络攻击,这一章会介绍几个网络安全上的防范重点。

    关于网络安全,有几点观念值得一提:

    • 不像做功能有就有,没有就没有。网络安全只能说相对比较安全。
    • 不需要花太多功夫,网站就可以有足够的安全性。但是如果需要极高的安全需求,花费的成本才会大幅提昇。安全性有时和使用性(usability)*有时是冲突的,想要越高的安全性可能导致功能越难用(想想验证码吧)。这在设计上需要取舍。
    • 安全性必须是设计软件一开始就必须考量到

    当然,还有一项最重要的网络安全黄金守则:「千万不要相信使用者输入进来的资料」。使用者是邪恶的,他们会有不预期的操作和输入不正常的资料。

    XSS可说是网站界第一名常见的攻击模式,恶意的使用者可以将脚本程式码放在网页上让其他使用者执行,任何可以让使用者输入资料的网站,都必须小心这个问题。例如可以将以下的程式贴到网页上:

    当一般使用者浏览到这一页时,就会跳出alert视窗,或是将敏感资料例如cookie内容传给攻击者。

    要防范这个问题的方法,就是要逸出使用者输入的内容,例如将变成&lt;script&gt;,使之显示出来的时候不让浏览器去执行。你可以会想只要逸出<script>就好了吧?这就错了,请千万不要尝试建立黑名单过滤,你可以参观这个网站,就会知道有非常多形式可以让浏览器去执行脚本程式。因此最简单又保险的方式,就是全部逸出。这在Rails 3版本已经变成默认行为,任何View样本的字串,都会做HTML逸出。

    如果你知道资料是安全的不要逸出,这时你要用html_saferaw方法:

    1. "<p>safe</p>".html_safe
    2. # 或
    3. raw("<p>safe</p>")

    Rails 3之前不会自动逸出,因此在样板中需要加escapeHTML()h()方法。也因为很多人常常会忘记造成XSS漏洞,所以在Rails 3之后就改成默认逸出了。

    关于如何在 Rails Helper 中正确处理逸出 HTML,请参考「Action View - Helpers 方法」的 如何安全地处理HTML逸出问题? 一节。

    但是有时候我们还是必须开放让使用者可以张贴简单的HTML内容,例如超连结、图片、标题等等。这时候我们可以用白名单的作法,Rails提供了sanitize()方法可以过滤逸出。

    跨站伪造请求CSRF(Cross-site request forgery)

    CSRF是说攻击者可以利用别人的权限去执行网站上的操作,例如删除资料。例如,攻击者张贴了以下脚本到网页上:

    1. <img src="/posts/delete_all">

    攻击者自己当然是没有权限可以执行”/posts/delete_all”这一页,但是网站管理员有。当网站管理员看到这一页时,浏览器就触发了这个不预期的动作而把资料删除。

    不过,这样还不够。因为即使是POST,浏览器还是可能不经过你同意而自动发送出去,例如:

    1. <a href="http://www.harmless.com/" onclick="
    2. var f = document.createElement('form');
    3. f.style.display = 'none';
    4. this.parentNode.appendChild(f);
    5. f.method = 'POST';
    6. f.action = 'http://www.example.com/account/destroy';
    7. f.submit();
    8. return false;">To the harmless survey</a>

    所幸,Rails内建了CSRF防御功能,也就是所有的POST请求,都必须加上一个安全验证码。在app/controllers/application_controller.rb你会看到以下程式启用这个功能:

    1. class ApplicationController < ActionController::Base
    2. protect_from_forgery with: :exception
    3. end

    这个功能会在所有的表单中自动插入安全验证码:

    1. <form action="/projects/1" class="edit_project" enctype="multipart/form-data" id="edit_project_1" method="post">
    2. <div style="margin:0;padding:0;display:inline">
    3. <input name="_method" type="hidden" value="patch" />
    4. <input name="authenticity_token" type="hidden" value="cuI+ljBAcBxcEkv4pbeqLTEnRUb9mUYMgfpkwOtoyiA=" />
    5. </div>

    如果POST请求没有带正确的验证码,Rails就会丢出一个ActionController:InvalidAuthenticityToken的错误。

    Layout中也有一段<%= csrf_meta_tags %>是给JavaScript读取验证码用的。

    SQL injection注入是说攻击者可以输入任意的SQL让网站执行,这可说是最有杀伤力的攻击。如果你写出以下这种直接把输入放在SQL条件中的程式:

    1. Project.where("name = '#{params[:name]}'")

    那么使用者只要输入:

    最后执行的SQL就会变成

    1. SELECT * FROM projects WHERE name = 'x'; DROP TABLE users; --’

    其中的结束了第一句,第二句DROP TABLE users;就让你欲哭无泪。

    要处理这个问题,也是一样要对任何有包括使用者输入值的SQL语句做逸出。在Rails ActiveRecordwhere方法中使用HashArray写法就会帮你处理,所以请一定都用这种写法,而不要使用上述的字串参数写法:

    1. Project.where( { :name => params[:name] } )
    2. # or

    如果你有用到以下的方法,ActiveRecord是不会自动帮你逸出,要特别注意:

    • find_by_sql
    • execute
    • where 用字串参数
    • group
    • order

    详见

    你可以自定一些固定的参数,并检查使用者输入的资料,例如:

    1. class User < ApplicationRecord
    2. def self.find_live_by_order(order)
    3. raise "SQL Injection Warning" unless ["id","id desc"].include?(order)
    4. where( :status => "live" ).order(order)
    5. end
    6. end
    1. class User < ApplicationRecord
    2. def self.find_live_by_order(order)
    3. where( :status => "live" ).order( connection.quote(order) )
    4. end
    5. end

    大量赋值(Mass assignment)

    Mass assignemet是个Rails专属,因为太方便而造成的安全性议题。ActiveRecord物件在新建或修改时,可以直接传入一个Hash来设定属性(这功能叫做Mass assignment),所以我们可以直接将网页表单上的参数直接丢进放进去:

    1. def create
    2. # 假设表单送出 params[:user] 参数是
    3. # {:name => “ihover”, :email => "ihover@gmail.com", :is_admin => true}
    4. @user = User.create(params[:user])
    5. end
    6. def update
    7. @user = User.update(params[:user])
    8. end

    但是这个Model包含一些敏感属性,例如此例中is_admin是个辨别是否是管理员的Boolean值,恶意的使用者可以直接修改HTML表单送出is_admin=true,造成了安全上的漏洞,所以以上的程式实际上会出现ActiveModel::ForbiddenAttributesError的安全错误讯息。

    为了解决这个问题,Rails使用了Strong Parameters的机制来检查params参数必须经过检查才可以做Mass assignment,例如上述的程式必须改成:

    1. def create
    2. @user = User.create(user_params)
    3. def update
    4. @user = User.update(user_params)
    5. end
    6. protected
    7. params.require(:user).permit(:name, :email)
    8. end

    这样才可以一次赋值nameemail

    当然,如果你没有Mass assignment的需求,大可不必用到Strong Parameters技巧,例如以下的程式也是可以运作的:

    当你需要根据使用者传进来的params[:id]做资料查询的时候,你需要注意查询的范围,例如以下是找订单:

    1. def show
    2. @order = Order.find(params[:id])
    3. end

    使用者只要随意变更params[:id],就可以查到别人的订单,你可能会写出以下的程式来防范:

    1. def show
    2. @order = Order.find(params[:id])
    3. if @order.user != current_user
    4. flash[:alert] = "你没有权限"
    5. redirect_to root_path
    6. return
    7. end
    8. end

    或是透过ActiveRecord限定范围即可:

    1. def show
    2. @order = current_user.orders.find(params[:id])
    3. end

    这样如果不是你的订单,就会变成找不到资料而已。

    敏感资讯处理

    网站的敏感资讯,例如密码、信用卡卡号等,请不要存在以下空间:

    • cookie
    • session
    • flash
    • 长时间放在内存中
    • Log档案
    • 快取

    其中Rails内建了log敏感资讯过滤的功能,在config/initializers/filter_parameter_logging.rb有一行这样的设定:

    1. Rails.application.config.filter_parameters += [:password]

    假设移除这一行,当使用者注册时输入密码,Log档案就会记录:

    1. Processing UsersController#create (for 127.0.0.1 at 2009-01-02 10:13:13) [POST]
    2. Parameters: {"user"=>{"name"=>"eifion", "password_confirmation"=>"secret", "password"=>"secret"}, "commit"=>"Register", "authenticity_token"=>"9efc03bcc37191d8a6dc3676e2e7890ecdfda0b5"}

    其中的原始password就会被记录下来的,非常地不好。如果套用上述的设定,Rails则会过滤成:

    1. Parameters: {"user"=>{"name"=>"susan", "password_confirmation"=>"[FILTERED]", "password"=>"[FILTERED]"}, "commit"=>"Register", "action"=>"create", "authenticity_token"=>"9efc03bcc37191d8a6dc3676e2e7890ecdfda0b5", "controller"=>"users"}

    这样就毫无记录了。

    • 这个 Gem 可以帮忙分析你的 Rails 有哪些可能的漏洞
    • 开发者需要经常注意使用的套件是否有安全性的更新,推荐可以使用bundle-audit这个工具进行检查,执行:

    之后,就会列出有哪一些gems有安全性更新。

    其他线上资源