Lua XSS: Examples
and Prevention

stackhawk

StackHawk|March 1, 2022

In this post, we'll take an in-depth look at how Lua XSS attacks work and how you can detect and prevent them.

Lua is a popular scripting language that's powerful in the hands of a seasoned developer. But it's very easy for a novice programmer to learn Lua while wielding a great deal of power. Unfortunately, that combination can lead to severe problems like vulnerabilities to cross-site scripting attacks (XSS) in web applications. 

Let's look at how Lua XSS attacks work and how you can detect and prevent them.

What Is Cross-Site Scripting (XSS)?

Cross-site scripting attacks trick web applications into sending harmful scripts to their web clients. Attackers use XSS to hijack user sessions, execute malicious code on a web server or in a user's web browser, launch phishing attacks, spread malware, and more. 

The attacker does this by feeding the malicious script to the application in the form of input to a form or API request. The application expects information about a user but instead receives executable instructions or a deliberately malformed URL. Attackers usually write their XSS exploits in Javascript snippets, but a skilled attacker can compromise a susceptible app in a URL request. 

Lua XSS: Examples and Prevention - Picture 1 image

What Does Lua Have to Do With XSS?

Lua is a scripting language that's easy to learn and embed in a host application. At the same time, it has access to the underlying operating system's system interface. It's garbage-collected, has powerful constructs based on associative arrays, and runs with bytecodes to make it faster. 

You probably don't think of Lua as code for front-end applications, though. Many people associate it with games like Minecraft and Roblox instead. You'll also find Lua used for server-side applications like Redis or extensible utilities like Pandoc. But in web apps? 

Developers use Lua in many different types of applications, including the web. Both Apache and Nginx, the two most widely used webservers, offer mod_lua and the lua-nginx-module, respectively. Also, even though there have been no updates to CGILua since 2015, it'll still install and run on modern Linux distributions. 

Suppose you want to go beyond CGI and the webservers' embedded capabilities? There's OpenResty, a web platform for using Lua with Nginx. The Kepler project offers Orbit, an MVC toolkit, and WSAPI, an abstraction layer for Lua. 

So, there are plenty of opportunities for an XSS attack to rear its ugly head with Lua. Let's look at how.

Lua XSS Attacks

Reflected XSS Attacks

A reflected attack is the most straightforward and common form of XSS attack.

They occur when a malicious script is "reflected" off a web application and into a user's web browser. The user will click on the link, expecting to post a reply, see an image or video, or go to a friendly website. 

But their browser executes a damaging script instead. Frequently the script sends the user's personal data to the attacker, hence the term "cross-site scripting." The compromised site directed you to another one. This attack is easy to launch, which is why it's so popular and effective. 

Another name for reflected XSS attacks is non-persistent attacks because they don't compromise the underlying application. Malefactors embed them in user-defined content like forum posts and comments. So, they're usually visible to a small audience, and attackers have to cast a wide net, hoping that a small number of victims will click on the malicious links. 

Lua and Reflected Attacks

Lua's simple syntax makes it easy for novice web programmers to open their app up to a reflected XSS attack.

Apache's mod_lua has two methods for echoing arguments passed to a handler back to the requestor. 

Mod_lua apps implement a handler(r) function, where r is a request object Apache passes to the function. 

function handle(r)
local danger = r:parseargs().user_name or ""
r:puts(danger)
end

The handler function extracts arguments passed to the web call with r:parseargs(). It writes content back to the requestor as an HTML response using r.puts() for strings and r.write() for unbuffered bytes. In neither case is the string sanitized or checked. If an attacker passes in Javascript or HTML content, the handler returns it verbatim. 

Nginx is susceptible to a similar attack. Its Lua code can be loaded dynamically or included in an OpenResty installation. But, it doesn't sanitize inputs. 

local danger = ngx.req.get_uri_args().user_name or ""
ngx.header.content_type = "text/html"
ngx.say(danger)

Say() eches the string back to the requestor as an HTML response. 

CGILua looks similar, too.

local danger = cgilua.QUERY.user_name or ""
cgilua.htmlheader()
cgilua.put(danger)

In all of these cases, the input should be verified before use.

Stored XSS Attack

In a stored attack, the hacker embeds malicious code in a comment in a user forum or on social media. These attacks are persistent since the web applications store them in a database or other permanent storage. They're more dangerous since many users may view the destructive content before the application owner discovers the attack. 

Rather than (or in addition to) echoing the malicious request back to the requestor, the application stores it. Subsequent visitors to the content request the same page, see the malicious data and click on it.

Here's a contrived code snippet that stores a comment in MySQL and then echoes back the complete set of comments for a given post id.

function handle(r) 

   -- connect to mysql
   driver = require "luasql.mysql"
   create environment object
   env = assert (driver.mysql())
   connect to data source
   con = assert (env:connect("mywebapp","root","123456"))

   -- grab post id, user, text
   local post_id = r:parseargs().comment_id or ""
   local user_name = r:parseargs().user_name or ""
   local comment_text = r:parseargs().comment_text or ""

   -- insert into the comments table
   res = assert (con:execute(string.format([[
      INSERT INTO comments
      VALUES ('%s', '%s', '%s)]], post_id, user_name, comment_text)
   ))

   -- probably do some more html things

   -- now pull all fothe comments out of the table and display
   -- cursor needs an order by 
   cur = assert (con:executestring.format([[SELECT user_name, comment_text from post_id = %s"]], post_id))
   row = cur:fetch ({}, "a")
   while row do
      r:puts(string.format("<p> User: %s<br> %s", row.user_name, row.comment_text))
      -- reusing the table of results
      row = cur:fetch (row, "a")
   end

   cur:close()
   con:close()
   env:close()
end

The user name, comment body, or both may contain malicious code in this example. It's echoed back to the user, but it's stored in MySQL, too! So any other users that view comments will see the dangerous script. 

Lua XSS: Examples and Prevention image

Preventing LUA XSS

Preventing these attacks in Lua web applications is relatively simple, and there are already tools to help you. 

Don't Trust User Input

In all of the above cases, the Lua scripts use request data without verifying its contents. This is the first and biggest mistake. At a minimum, you must "escape" any data you receive before storing or passing it back to a web user. Lua doesn't have a built-in method for sanitizing web input, so you have to roll your own or rely on the user community. 

Web_sanitize is an HTML filter for Lua. You can download it from GitHub or install it with luarocks. The code is too long to embed here, but if you look at it (and you should before adding it to your app), you can see how it sanitizes and escapes user input so a malicious script will no longer execute in a client browser. You can escape HTML and CSS or extract text from a request, stripping away any embedded code. 

Here's a safe version of the Apache Lua handler.

function handle(r)
local web_sanitize = require "web_sanitize"
local safe = web_santize.extract-text(r:parseargs().user_name) or ""
r:puts(safe)
end

Validate User Input

Another approach is to validate input before using it, but this isn't an alternative to sanitizing input. It's an extra step. After you've stripped out any potential scripting code, you can ensure the input makes sense in context. So, verifying a credit card's checksum or a user's address before storing them. It's not necessarily protection against XSS, and it can be challenging to validate input without creating new problems, especially with regards to international names. Still, it's a good idea when dealing with user-specified code and formatting data.

Automated API security testing in CICD

Guarding Against Lua XSS

In this post, we looked at Lua XSS. We saw how developers use Lua for web applications and how the language doesn't have built-in facilities for sanitizing user input before using or storing it. After looking at three examples of code that's vulnerable to attack, we looked at a simple way to protect it. 

StackHawk has tools for detecting these kinds of vulnerabilities. By adding them to your CI/CD pipeline, you can discover them before they become problems. Sign up for a free trial today! 

This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!).


StackHawk  |  March 1, 2022