Web applications are vulnerable to several kinds of attacks, but they’re particularly susceptible to code injection attacks. One such attack, the XPath Injection, takes advantage of websites that require user-supplied information to access data stored in XML format. All sites that use a database in XML format might be vulnerable to this attack.
XPath is a query syntax that websites can use to search their XML data stores. When properly executed, XPath queries are a legitimate means of locating data held by elements and attributes. Websites using databases with XML formatted data integrate user input (for example, a username and password) into XPath queries to locate the required data.
However, similar to other injection attacks (such as the popular SQL injection attack) a malicious actor can exploit this process using malformed information. An attacker can find out how the XML data is structured and access data they may not normally have access to. Attackers who obtain this information can elevate their access privileges (an “escalation of privilege” attack), possibly compromising the application and sensitive data and using the information or access to attack the rest of the organization.
Beyond data deletion or corruption, data exfiltration can enable hackers to run application commands, provide entry for further server attacks, and bring down the entire organization with other attacks. The consequences of these attacks can damage or destroy the reputation of our applications and a company’s credibility with its clients.
Fortunately, we can protect our sites against XPath injection attacks. In this article, we’ll explore XML vulnerabilities and learn how to prevent them from compromising application data.
Identifying and correcting XPath injection vulnerabilities
To see how we can prevent XPath injections, we’ll need to examine one. Then, we’ll learn how best to protect our apps and data from this attack.
Exploring a vulnerable authentication app
Let’s use an application requiring users to log in with a username and password.
Create a folder named xpath
and inside this folder, create a file named users_info.xml
with the XML data below:
1
jennifer
4uyFh6v0
757456
2
kimkimani
6wreD1678
870965
3
michael
gf5dG63g
345689
4
robert
yH8wej3h
096417
Inside the 0xpath
folder, create a new file, name it main.py
, and paste the following code into the file:
# Import the required modules
from lxml import etree
# Parse the XML data
tree = etree.parse("users_info.xml")
def login(username, password):
expression = "/users/userinfo[username='" + username + "' and password='" + password + "']"
results = tree.xpath(expression)
# if no results, which means login details did not match
if len(results) < 1:
print("Incorrect credentials! Check your username or password.")
# if login details match
else:
for result in results:
print(f'Login successful! username: {result[1].text}, password: {result[2].text}')
# Get the user-provided input
username = input("Enter your username: ")
password = input("Enter your password: ")
login(username, password)
The code loads the XML data, and then executes an XPath query to select the XML data that matches the user-provided credentials. If it finds a match, it authenticates the user. If it doesn’t find a match, the app displays the message, “Invalid credentials! Check your username or password.”
Before testing this code, install the Python lxml package for processing XML data. Run this command to install it:
pip install lxml
To test the code, call the login
function and pass your username and password. You can do this by adding this code snippet to the bottom of your main.py
file:
login(username="kimkimani", password="6wreD1678")
To execute the file, run the command below in the terminal.
python3 main.py
A success message is displayed. You can also try to log in with incorrect credentials, which results in a failure message.
This code is vulnerable to XPath Injection attacks because it links the user input to the XPath query, modifying the structure of the query and injecting malicious code.
A typical XPath query is a text string specifying the elements you want to select from XML data.
In the code above, the query "/users/userinfo[username='" + username + "' and password='" + password + "']"
selects all userinfo
elements that match the user-supplied username and password.
However, supplying a username and password containing special characters or keywords used in XPath queries modifies the structure of the query, and allows an attacker to bypass the login system. For example, supplying "' or 1=1"
as both username and password results in the following XPath query:
//users/userinfo[username = '' or 1=1' and password = '' or 1=1' ]
This query selects all the userinfo
elements with a username
and password
attribute that are either empty or equal to "1=1"
. The logical expression "1=1"
always evaluates to true
regardless of the actual values of the username
and password
attributes.
Suppose an attacker wants to gain access without a correct username or password. Instead of valid credentials, the attacker could insert any of the following XPath injections into the login form as both the username and password:
'or'1'='1
' or 1=1 or 'a'='a'
' or ''=''
' or '1'='1'
'a' or true() or '
'text' or '1' = '1'
Each of the above statements successfully bypasses application security by exploiting the logic and structure of XPath queries because all the XPath queries formed from the above inputs always evaluate to true. Also, assuming attackers know or can guess a username (for example, kimkimani
), they can run the following query:
kimkimani' or '1'='1
When we run this query in the form of a username
and password
, the resulting query selects the userinfo
element with the username kimkimani
, any userinfo
element named kimkimani
or '1'='1'
, or any userinfo
element where the value of the username
attribute is '1'='1'
and the value of the password attribute is '1'='1'
. Since all userinfo
elements meet these conditions, the login system allows the attacker to log in.
Furthermore, depending on the XML document’s structure, an attacker can send an injection based on the node positions as follows:
'or position()=4 or'
This query allows attackers to bypass authentication based on the user’s position in the XML document’s structure. The position()
function is a built-in XPath function that returns the position of the current node relative to its parent. The above expression evaluates to true
for the fourth node in the XML document allowing the attacker to log in.
Preventing XPath injection vulnerabilities
While there is no shortage of ways to exploit insecure XML data, we have several modes of alleviating these vulnerabilities within our websites and applications. Let’s explore a few.
Sanitize inputs
One strategy for preventing XPath Injection attacks is to filter the characters that users can input. Although it’s not foolproof, this method ensures that we block inputs that could form the bases of injection queries.
To implement this approach, create a file, regex.py
, inside the xpath
folder and paste the following code into the file.
# Import the required modules
from lxml import etree
import re
# Parse the XML data
tree = etree.parse("users_info.xml")
# Create an XPath evaluator
evaluator = etree.XPathEvaluator(tree, namespaces=None)
# login function
def login(username, password):
if len(re.findall('[^a-zA-Z0-9]', username)) > 0 or ⏎ len(re.findall('[^a-zA-Z0-9]', password))>0:
print("Suspected characters detected! Login failed!")
return False
else:
expression = "/users/userinfo[username='" + username + "' and password='" +⏎ password + "']"
results = tree.xpath(expression)
# if no results, which means login details did not match
if len(results) < 1:
print("Incorrect login details!")
# if login details match
else:
for result in results:
print("Login successful for user", result[1].text)
login(username="kimkimani", password="6wre1678")
This code uses a regular expression (regex) to detect all the characters that are not numeric or alphabetic. When any of those characters are detected, a “login failed” message appears in the terminal, and we immediately return False
to the consuming function to prevent further code execution. We can test the code with the characters used for the XPath injection discussed in the section above.
While this approach enhances our data security, input validation only filters out the characters we remember to include in sanitizing functions. Furthermore, password input values should have special characters, so we don’t want to block these inputs.
So, let’s explore some alternative methods of avoiding XPath injection vulnerabilities.
Use parameterized (prepared) XPath queries
To demonstrate this approach, create the file params.py
inside the xpath
folder and paste in the following code:
# Import the required modules
from lxml import etree
# Parse the XML data
tree = etree.parse("users_info.xml")
def login(username, password):
expression = "/users/userinfo[username=$username and password=$password]"
results = tree.xpath(expression, username=username, password=password)
# if no results, which means login details did not match
if len(results) < 1:
print("Invalid credentials! Check your username or password.")
return False
# if login details match
else:
for result in results:
print(f'Login successful! username: {result[1].text}, password:⏎ {result[2].text}')
login(username="kimkimani", password="6wreD1678")
Parameterized XPath queries helps us avoid linking the user input to the XPath query — passing it as a parameter instead. Apart from making the queries more secure, parameterized XPath queries are more flexible and reusable. However, they don’t prevent all injections. So, let’s look at another preventative measure.
Use precompiled XPath queries
Precompiled XPath queries aren’t constructed from user-supplied data. Therefore, they’re the only fully secure way to prevent injection attacks. To see how precompiled XPath queries help, create a file named compiled.py
and add this code:
import lxml.etree as et
xml = et.parse("users_info.xml")
find = et.xpath("/users/userinfo[username=$username and password=$password]")
results = find(xml, username="'or'1'='1", password="'or'1'='1")
print(results)
The code uses the etree.xpath
function to compile an XPath query, which it then stores in the find
variable. We can execute this compiled query multiple times using the find
function without having to recompile the query each time. This option is the most effective, as we don’t have to worry about any characters we should have escaped.
Best practices for spotting and avoiding XPath injection vulnerabilities
In addition to using the preventative measures above, of which precompiled queries are the best practice, we should implement the following best practices for spotting and avoiding XPath injection vulnerabilities.
Check packages and code for vulnerabilities
Use a tool like Snyk Open Source Advisor to scan open source packages we use for potential XPath injection vulnerabilities. Providing a comprehensive health overview, Snyk Advisor allows us to feel more confident in the security of our web app.
Similarly, we can use Snyk Code to perform source code scans and identify potential security vulnerabilities, including XPath injections. Snyk Code can quickly and easily identify any potential vulnerabilities in our code and take steps to fix them.
The screenshot below shows the results of performing a source code scan of the project folder using Snyk Code.
Avoid linking user-input and XPath queries
Don’t concatenate user-supplied input directly into XPath queries. This is one of the most common ways XPath injection vulnerabilities occur. Instead, using parameterized XPath queries allows the use of placeholders for user-supplied input. This can make code more readable and maintainable, making it easier to sanitize user-supplied input and prevent XPath injection attacks.
Be conscious of special characters
If we can’t use parameterized queries, properly escape any special characters in user-supplied input before using it in an XPath query.
Implement prepared statements and binding variables
Using prepared statements and binding variables in XPath queries can help prevent an attacker from injecting arbitrary XPath queries into an application's input, as it treats the input as a variable value rather than part of the query itself.
Review code regularly
In addition to using tools like Snyk Advisor and Snyk Code, regularly reviewing and testing code for potential XPath injection vulnerabilities is essential. This can help identify and fix potential vulnerabilities before attackers exploit them. We can use manual code review, automated code analysis tools like Snyk Code, and penetration testing to identify and correct code vulnerabilities.
Conclusion
XPath injection can severely harm websites, data, and organizations’ reputations and risk access to future breaches. However, with a little extra effort, we can protect our applications by using tools like Snyk Advisor and Snyk Code, following best practices to spot and avoid XPath injection vulnerabilities in our code, and patching vulnerabilities with care.
We can secure our applications against XPath injection attacks by properly sanitizing user-supplied input, using parameterized XPath queries, and especially by using precompiled XPath queries — which aren’t constructed from user-supplied data making them the only fully secure way to prevent injection attacks. These preventative measures are essential and can’t be overlooked, given the consequences of XPath injection attacks.