excel hacked

Fixing the case of the un-sanitised input web app


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;
}