Navigating the Dangers: User Input Risks in Apigee Target URLs

Understanding and mitigating the hidden vulnerabilities of user input in Apigee target URLs.

In today's interconnected digital world, APIs are the glue that holds various services and applications together. Apigee, a robust API management platform, empowers companies to ensure secure and efficient interactions between clients and services. However, the security of an API can be compromised if user input isn't handled properly, especially when it ends up in the target system's URL.

In this post, we'll dive into the risks associated with incorporating user input into the target system's URL and explore ways to prevent potential attacks. Keep in mind that the risks aren't limited to what we'll discuss here; other threats like Server-Side Request Forgery (SSRF) and even Denial of Service (DoS) attacks can also arise from improper handling of user input within the target URL.

How Does Manipulation of the Target URL Typically Occur?

A common practice in Apigee is dynamically constructing the target URL based on user input. While this approach is convenient for routing requests or customizing responses, it introduces potential security vulnerabilities if not managed carefully.

Let's take a look at an example from the Apigee documentation:

if (context.flow == "PROXY_REQ_FLOW") {
    var username = context.getVariable("request.formparam.user");
    context.setVariable("info.username", username);
}

if (context.flow == "TARGET_REQ_FLOW") {
    context.setVariable("request.verb", "GET");
    var name = context.getVariable("info.username");
    var url = "http://mocktarget.apigee.net/";
    context.setVariable("target.url", url + "?user=" + name);
    context.setVariable("request.content", "");
}

In this script the user parameter is extracted from the incoming request's form data and stored in a flow variable called info.username. This variable is used later in the target request flow - by appending the user parameter to the base URL an updated target URL is created.

What Can Go Wrong Here?

The issue with the above code is that it literally allows the injection of new parameters into the target URL, which also known as HTTP Parameter Pollution. This happens because user input is directly appended to the URL without proper validation or encoding.

For instance , consider the following form submission using curl

curl -X POST \

  'https://your-apigee-endpoint.com/target_url' \

  --header 'Accept: */*' \

  --header 'Content-Type: application/x-www-form-urlencoded' \

  --data-urlencode 'user=John&new_param=new_value'

In this request, the user parameter includes an additional &new_param=new_value. When the script processes this input, the target.url becomes http://mocktarget.apigee.net/?user=John&new_param=new_value!

Pay attention to how the new target.url looks like!

This means that the additional parameter new_param with the value new_value is unintentionally included in the target URL.

But the problems don't stop at query parameters. Malicious actors might also perform a path traversal attack.

Moving User-Controlled Data to the URL Path

Let me share a real-life example I encountered during a security assessment. A developer extracted parameters using the ExtractVariables policy and then constructed the target.url using the AssignMessage policy (BTW, as per Apigee documentation using AssignMessage is the second convenient way along with JavaScript policy to override the target URL). While this method is also flexible as the JavaScript policy approach, it can introduce same security vulnerabilities if not implemented carefully.

The developer used the ExtractVariables policy to capture the value of a custom header:

<ExtractVariables name="Extract-SubscriptionType">
    <Source>request</Source>
    <Header name="X-Subscription-Type">
        <Pattern>{subscriptionType}</Pattern>
    </Header>
    <VariablePrefix>flow</VariablePrefix>
</ExtractVariables>

This simple policy extracts the X-Subscription-Type header from the incoming request and stores it in the variable flow.subscriptionType.

Next, the AssignMessage policy was used to construct the target.url by incorporating the extracted header value into the URL path:

<AssignMessage name="Set-TargetURL">
    <AssignVariable>
        <Name>target.url</Name>
        <Template>https://backend.example.com/billing/v2/{flow.subscriptionType}/data</Template>
    </AssignVariable>
</AssignMessage>

In this setup, the value of flow.subscriptionType is directly inserted into the URL path without any validation or sanitisation. The intention was to route requests based on the user's subscription type, such as "payg" or "paym".

After setting up the policies, let's look at two examples to illustrate how this approach can lead to vulnerabilities.

Example 1: Normal Request

A user with a standard subscription sends a request with the header:

X-Subscription-Type: payg

The ExtractVariables policy captures the value payg and stores it in flow.subscriptionType. The AssignMessage policy then constructs the target.url:

https://backend.example.com/billing/v2/payg/data

This URL correctly routes the request to the intended backend service for "pay-as-you-go" customers. The system behaves as expected, delivering the appropriate data!

Example 2: Path Traversal Attack

An attacker manipulates the X-Subscription-Type header to include a path traversal sequence:

X-Subscription-Type: ../../internal/v1/call#

Again, the ExtractVariables policy captures this value and stores it in flow.subscriptionType. The AssignMessage policy constructs the target.url:

https://backend.example.com/billing/v2/../../internal/v1/call#/data

Due to the ../../ in the path, when Apigee resolves this URL, it effectively navigates up two directory levels, while the # trims /data, resulting in:

https://backend.example.com/internal/v1/call

This means the request now targets the /internal/v1/call endpoint instead of the intended /billing/v2/payg/data. That's how the attackers gain unauthorised access to internal resources!

Thanks Apigee Debug tool we can see the URI and confirm the issue

Extracting Flow Variables

Building on our previous examples, there's a critical aspect to consider when constructing the target.url in Apigee: the way Apigee assigns a new value to target.url

Regardless of how you build target.url (with AssignMessage, JavaScript or other policy), at the moment you assign a new value to it, Apigee treats that value as a message template and evaluates any variables within it. This behaviour can lead to unexpected variable substitutions if user-controlled data includes variable placeholders, potentially causing serious security vulnerabilities.

Let's delve into how this happens. Consider the AssignMessage policy where you assign a new value to target.url using a template:

<AssignVariable>
    <Name>target.url</Name>
    <Template>https://backend.example.com/billing/v2/{flow.subscriptionType}/data</Template>
</AssignVariable>

Here, the process can be viewed as two separate operations - template variables resolution and assignment with additional evaluation.

Template Variables Resolution:
  1. The <Template> element processes any variables enclosed in curly braces {}.
  2. If flow.subscriptionType is "payg", the template resolves to https://backend.example.com/billing/v2/payg/data
  3. Let's assume this resolved value is temporarily stored in a variable, say target.url.temp.
Assignment with Additional Evaluation:
  1. When target.url is assigned the value of target.url.temp, Apigee treats the assigned value as a message template again.
  2. This means any variable placeholders within target.url.temp are evaluated a second time.
  3. If target.url.temp contains {flow_variable}, Apigee attempts to resolve it, potentially substituting it with a sensitive value.

Suppose an attacker sends a request with the header:

X-Subscription-Type: {private.client_secret}

The target.url will first become:
https://backend.example.com/billing/v2/{private.client_secret}/data.
Then, during the assignment, Apigee evaluates {private.client_secret} and substitutes it with the actual value of private.client_secret, resulting in:
https://backend.example.com/billing/v2/AY62KADL37yAgd981q/data

This means that the private.client_secret value, which is supposed to be confidential, is now included in the URL sent to the backend.

How it looks in Apigee Debug session

Furthermore, if the backend responds with an error message like:

{"error": "AY62KADL37yAgd981q is not a valid subscription type"}

The attacker receives the client_secret value in the response, effectively disclosing sensitive information intended for secure communication with the target system. 

What to Do? How to Mitigate?!

By now, you might be thinking, "Wow, user input can really throw a wrench into the works!"

And you'd be absolutely right. But fear not - there are effective strategies you can employ to protect your Apigee APIs from these vulnerabilities. Let's explore how you can fortify your API defences and keep those malicious actors away.

Validate User Input Like Your API Depends on It - Because It Does!

Imagine you're throwing a VIP party. Would you let just anyone walk in off the street? Probably not. You'd check their invitation to ensure they're on the guest list. Similarly, you should never trust user input without verifying it first.

Whitelist Acceptable Values

Only allow expected values. For example, if subscriptionType should be either "payg" or "paym", check explicitly for these.

var subscriptionType = context.getVariable('flow.subscriptionType');
var validTypes = ['payg', 'paym'];

if (validTypes.indexOf(subscriptionType) == -1) {
    // Reject the request with an error
    throw new Error('Invalid subscription type');
} else {
    // Proceed with the valid input
}
Use Regular Expressions

Validate the format of input data to ensure it meets the expected pattern.

<Step>
    <Name>ExtractVariables.UserID</Name>
</Step>
<Step>
    <Name>RaiseFault.InvalidUserID</Name>
    <Condition>!(flow.userId JavaRegex ^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89AB][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$)</Condition>
</Step>

Encode User Input - Wrap It Up Safely

Think of encoding as bubble wrap for your data. It protects your application by neutralising potentially harmful input!

Before inserting user input into URLs, encode it using encodeURIComponent() in JavaScript.

var subscriptionType = context.getVariable('flow.subscriptionType');
var encodedSubscriptionType = encodeURIComponent(subscriptionType);
var targetUrl = 'https://backend.example.com/billing/v2/' + encodedSubscriptionType + '/data';
context.setVariable('target.url', targetUrl);

Educate Your Team - Security is a Team Sport

Your API is only as secure as the people who develop and maintain it. Make sure everyone is on the same page. Regularly update your team on the latest security best practices and vulnerabilities. Incorporate security checks into your code review process - a fresh pair of eyes might catch something you missed.


Take Control of API Security with CodeSent

Securing APIs from user input vulnerabilities is crucial for preventing unauthorised access, data leakage, and exploitation. While manual checks and best practices are important, having automated solutions can greatly strengthen your defence. CodeSent offers comprehensive analysis for Apigee environments, including detecting when user input is improperly sunk into target URLs through taint analysis.

Ready to safeguard your Apigee configurations? Learn how CodeSent can help you identify and mitigate these risks effectively by visiting this detailed rule page.


Request a Demo

Navigating the Dangers: User Input Risks in Apigee Target URLs
CodeSent, Nikita Markevich 4 November 2024
Share this post
Archive
Dealing with Path Parameters
And Why You Should Stop Using MatchesPath (at least for static paths!)