Last year I met this web application. Let’s call it Hank. Hank accepted user input, without sanitising it. The administrator of Hank had a reports interface, which was generated as a comma-separated-values (CSV) file and downloaded. The security issue here is that the user’s desktop is likely configured to open CSV files in Excel. Since the input from unknown users ends up in this CSV, that could end up being interpreted as an equation. In Excel it is possible for equations to execute external commands, e.g. =cmd|' /c notepad'
will open notepad on the Desktop. And, there are worse things than notepad.
Since the administrator is likely inside the corporate firewall, this means an increased risk: a malicious actor, outside the firewall, can now use this as a vector to run code inside the firewall.
To complicate matters further, the library generating the reports (Crystal Reports) had no ability to sanitise the data on output. The customer did not wish to change the source code to their application to try and sanitise the data on the input.
Challenged to solve this complex problem we turned to the Agilicus Web Application Firewall. By writing complex rules in Lua we could redirect flows, or do simple sanitising. However, we did not feel this would be sufficient: if bad data got in the database, it would always generate a risky report. Wanted to do the processing in the output chain.
To solve the problem we developed a filter (using Python and the xlrd library), running as a web service. It accepted, via a POST, an xls or csv file. It would then scrub it, quoting anything that looked like a formula, and return the result.
We then configured a rule in the Web Application Firewall so that, when the user generates a report, the output of the web application is silently run through this filter, and then returned to the user. The result is completely transparent: no change in functionality. However it is safe: there is no circumstance where the administrator, running a report, need worry about it attacking their desktop.
At the end we show the first bit of complexity, trapping and returning a different file, transparently to the user. We do this in OpenResty with a location block and some Lua. Learn more of the other techniques about getting .NET to the Net.
location ~ ^/[^/]*/Export {
access_by_lua_file "/rules/fix-crystal.lua";
proxy_max_temp_file_size 0;
content_by_lua_block {
if ngx.status ~= 401 then
local upstream_src = ngx.location.capture('/_crystal/'..ngx.var.request_uri)
if upstream_src then
local args, err = ngx.req.get_uri_args()
if args['ReportFormat'] ~= nil and args['ReportFormat'] ~= "Excel" then
ngx.header["Content-Type"] = "application/pdf"
ngx.header["Content-Disposition"] = "attachment; filename=report.pdf"
ngx.say(upstream_src.body)
else
if xls_token == nil then
ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE
ngx.say("Error: xls filter token unavailable.")
ngx.exit(ngx.OK)
else
local http = require "resty.http"
local httpc = http.new()
local ok, err = httpc:request_uri("https://xls-filter?token="..xls_token, {
method = "POST",
body = upstream_src.body,
headers = {
["Content-Type" ]= "application/vnd.ms-excel",
},
ssl_verify = true
})
if not ok then
ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE
ngx.say("Error: xls filter service unavailable.")
ngx.exit(ngx.OK)
elseif ok.status ~= 200 then
ngx.say("Error: xls filter detected problem with file: "..ok.body)
ngx.exit(ngx.OK)
else
ngx.header["Content-Type"] = "application/vnd.ms-excel"
ngx.header["Content-Disposition"] = "attachment; filename=report.xls"
ngx.say(ok.body)
end
end
end
else
ngx.status = ngx.HTTP_SERVICE_UNAVAILABLE
ngx.say("Error: crystal reports service not available.")
ngx.exit(ngx.OK)
end
end
}
}
location /_crystal/ {
proxy_max_temp_file_size 0;
fastcgi_hide_header X-AspNet-Version;
fastcgi_hide_header X-AspNetMvc-Version;
fastcgi_hide_header X-Powered-By;
fastcgi_index Index.html;
rewrite ^/_crystal(.*) $1;
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
}