本文最后更新于 2024-07-06,文章内容可能已经过时。

问题描述

今天一大早公司的项目群里接到了省里的风险报告需要整改。其中有一项是:

风险名称OSS读写权限风险
风险描述所有人都可以对Object进行匿名读写、读操作,可能导致bucket存储空间中被写入"恶意"文件,或者敏感文件泄露。
修复建议1.关闭公共读写、读权限,修改为私有权限;
2.采用带签名的方式进行oss bucket中文件对象的读取;

原来是我们有一个项目的OSS是公共读,项目前端直接使用OSS地址所导致的。这里比较麻烦,因为如果设置成了私有读的,资源连接将会变成:https://xxx.aliyuncs.com/temp.jpg?Expires=xxx&OSSAccessKeyId=xxx&Signature=xxx,后面的三个参数都为动态获取的,所有的连接都是具有时效性的,属实不好操作。

image-20220903234653337

Nginx有四个主流分支,其中OpenResty分支就是在普通的Nginx上增加了lua脚本的支持,这里我们是否可以通过lua脚本达到无感的操作,所以百度了一下很简单的就找到了对应的方法

准备工作

  1. 使用OpenResty替换为原来的Nginx
  2. 准备好OSS的基本信息

具体操作

安装OpenResty

具体的安装资料可以在OpenResty官方获取到,我这里是CentOS7,这里直接使用官方提供的预编译包了。

# 对于 CentOS 8 或以上版本,应将下面的 yum 都替换成 dnf
# 添加yum源
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/
# 更新
sudo yum check-update
# 安装OpenResty
yum install -y openresty

安装后工作目录在:/usr/local/openresty/nginx

image-20220903234036815

添加lua脚本

这里我直接在OpenResty工作目录下面增加了lua文件夹。

cd /usr/local/openresty/nginx
mkdir lua
cd lua
touch oss_auth.lua

将一些脚本内容拷贝进去

-- has been sorted in alphabetical order
local signed_subresources = {
    'acl',
    'append',
    'bucketInfo',
    'cname',
    'commitTransition',
    'comp',
    'cors',
    'delete',
    'lifecycle',
    'location',
    'logging',
    'mime',
    'notification',
    'objectInfo',
    'objectMeta',
    'partData',
    'partInfo',
    'partNumber',
    'policy',
    'position',
    'referer',
    'replication',
    'replicationLocation',
    'replicationProgress',
    'requestPayment',
    'response-cache-control',
    'response-content-disposition',
    'response-content-encoding',
    'response-content-language',
    'response-content-type',
    'response-expires',
    'restore',
    'security-token',
    'tagging',
    'torrent',
    'uploadId',
    'uploads',
    'versionId',
    'versioning',
    'versions',
    'website'
 }
 
 function string.startswith(s, start)
    return string.sub(s, 1, string.len(start)) == start
 end
 
 local function get_canon_sub_resource()
    local args = ngx.req.get_uri_args()
    -- lower keys
    local keys = {}
    for k, v in pairs(args) do
       keys[k:lower()] = v
    end
    -- make resource string
    local s = ''
    local sep = '?'
    for i, k in ipairs(signed_subresources) do
       v = keys[k]
       if v then
          -- sub table
          v = type(v) == 'table' and v[1] or v
          s = s .. string.format("%s%s=%s", sep, k, v)
          sep = '&'
       end
    end
    return s
 end
 
 local function get_canon_resource()
    resource = ''
    object = ngx.unescape_uri(ngx.var.uri)
    sub = get_canon_sub_resource()   
    return string.format("/%s%s%s", ngx.var.oss_bucket, object, sub)
 end   
 
 local function get_canon_headers()
    -- default: <lowerkey, value>
    local headers = ngx.req.get_headers()
    local keys = {}
    for k, v in pairs(headers) do
       if string.startswith(k, 'x-oss-') then
          -- client must assemble the same header keys
          if type(v) ~= 'string' then return nil end
          table.insert(keys, k)
       end
    end
    -- sorted in alphabetical order
    table.sort(keys)
    for i, key in ipairs(keys) do
       keys[i] = key .. ':' .. headers[key] .. '\n'
    end
    return table.concat(keys)
 end
 
 local function calc_sign(key, method, md5, type_, date, oss_headers, resource)
     -- string_to_sign:
     -- method + '\n' + content_md5 + '\n' + content_type + '\n'
     -- + date + '\n' + canonicalized_oss_headers + canonicalized_resource
     local sign_str = string.format('%s\n%s\n%s\n%s\n%s%s',
     method, md5, type_,
     date, oss_headers, resource)
     ngx.log(ngx.ERR, "SignStr:", sign_str, "\n")
     local sign_result = ngx.encode_base64(ngx.hmac_sha1(key, sign_str))
     return sign_result, sign_str
 end   
 
 local function oss_auth()
    -- ngx.log(ngx.INFO, 'auth')
    --local method = ngx.var.request_method
    local method = ngx.req.get_method()
    local content_md5 = ngx.var.http_content_md5 or ''
    local content_type = ngx.var.http_content_type or ''
    -- get date
    local date = ngx.var.http_x_oss_date or ngx.var.http_date or ''
    if date == '' then
       date = ngx.http_time(ngx.time())
       -- ngx.log(ngx.INFO, 'Date:', date)
       ngx.req.set_header('Date', date)
    end
    local resource = get_canon_resource()
    local canon_headers = get_canon_headers()
    local sign_result, sign_str = calc_sign(ngx.var.oss_auth_key, method, content_md5,
    content_type, date, canon_headers, resource)
    -- ngx.log(ngx.INFO, 'sign string:', sign_str)
    -- ngx.log(ngx.INFO, 'sign string len:', string.len(sign_str))
    local auth = string.format("OSS %s:%s", ngx.var.oss_auth_id, sign_result)
    ngx.req.set_header('Authorization', auth)
    ngx.exec("@oss")
 end   
 
 -- main
 res = oss_auth()
 
 if res then
    ngx.exit(res)
 end

脚本中ngx.exec("@oss")@oss就是到时候OpenResty配置文件中的标识,可以不用替换。

更改OpenResty配置文件

默认的主配置文件在:/usr/local/openresty/nginx/conf/nginx.conf,可以重新include之前的配置文件。下面贴一个示例的配置文件:

server {
    listen       80;
    server_name  localhost;
    charset utf-8;
    client_max_body_size 100m;
    #access_log  logs/host.access.log  main;		

    gzip on;
    gzip_min_length 1k;
	gzip_buffers 4 16k;
	#gzip_http_version 1.0;
    gzip_comp_level 8;
    gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
    gzip_vary on;
    gzip_disable "MSIE [1-6]\.";
			
    location / {
        # 下面四个参数改成你自己的
        set $oss_bucket "预设OSS路径";
        set $oss_auth_id "AccessKeyId";
        set $oss_auth_key "AccessKeySecret";
        rewrite_by_lua_file "/usr/local/openresty/nginx/lua/oss_auth.lua";
    }
    
    # 脚本中ngx.exec("@oss") 双引号中的标识
    location @oss {
        # OSS的域名
        proxy_pass  http://xxx.aliyuncs.com;
        # #表示最大上传10M,需要多大设置多大。
        client_max_body_size    10m; 

        if ( $request_method = 'OPTIONS' ) {
            add_header Access-Control-Allow-Origin *;
            add_header Access-Control-Allow-Methods 'POST,GET,PUT,OPTIONS';
            add_header Access-Control-Max-Age 3600;
            add_header Access-Control-Allow-Headers 'Origin,X-Requested-With,Content-Type,Accept,Authorization,sourcetype,token';
            add_header Access-Control-Allow-Credentials true;
            add_header Content-Length 0;
            add_header Content-Type text/plain;
            return 200;
        }

        proxy_redirect     off;
        # Host要修改为OSS的域名或OSS控制台绑定的域名,否则OSS无法识别会报错
        proxy_set_header   Host         xxx.aliyuncs.com;
        proxy_set_header   X-Real-IP    $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_max_temp_file_size 0;
        proxy_connect_timeout      90;
        proxy_send_timeout     90;
        proxy_read_timeout     90;
        proxy_buffer_size      4k;
        proxy_buffers          4 32k;
        proxy_busy_buffers_size    64k;
        proxy_temp_file_write_size 64k;
    }
}

重启OpenResty

systemctl restart openresty

测试

image-20220904000323724