Dealing with Path Parameters

And Why You Should Stop Using MatchesPath (at least for static paths!)

Apigee allows developers to define conditional behaviour with operators like MatchesPath, Matches, JavaRegex, ==, and others, enabling them to control which flows or policies apply based on specific request variables like the request path. Each operator has unique behaviour: while Matches lets you use * as a wildcard character, MatchesPath allows matching for specific path parts, and JavaRegex applies regular expressions.

Of all these operators, MatchesPath stands out to me, particularly for its behaviour with URL path parameter separators like commas, semicolons, and equal signs

URL Path Parameter Separators?

You might be wondering, what exactly are these separators? Well, URL path parameter separators such as semicolons (;), commas (,), and equals signs (=) are reserved characters used to encode extra details about a path segment. According to RFC 3986, these characters can indicate parameters or subcomponents relevant to a given segment.

For example, name;v=1.1 might refer to version 1.1 of "name," while name,1.1 could represent a similar concept. The use and interpretation of these separators often depend on the URI scheme or the implementation handling the URI, making them flexible yet potentially controversial in specific contexts. 

So, these separators can make URLs more descriptive, but they also introduce some ambiguity, which is especially relevant when securing API paths in Apigee.

How Apigee Handles Path Parameter Separators

Let's dive into how Apigee behaves when working with path parameters and separators. We'll use an ExtractVariables policy to see how Apigee extracts values from different types of requests.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ExtractVariables continueOnError="false" enabled="true" name="EV-1">
  <DisplayName>EV-1</DisplayName>
  <Properties/>
  <URIPath>
    <Pattern ignoreCase="true">/a/{id}</Pattern>
  </URIPath>
  <IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
  <Source clearPayload="false">request</Source>
  <VariablePrefix>apigee</VariablePrefix>
</ExtractVariables>

This policy aims to capture the id from the path /a/{id} and store it in apigee.id. Now, let’s test various request formats to see how Apigee extracts the value!

1️⃣ Request to /a/before - the policy correctly extracted the value of id as before.

2️⃣ Request to /a/before;after - despite the additional ;after, the policy still extracted id as before.

3️⃣ Request to /a/before=after - once again, only before was considered for the id extraction.

4️⃣ Request to /a/before,after - the result remained consistent, with id extracted as before.

It seems Apigee consistently ignores any parameters after the path separators when extracting variables with this policy. In each test case, the ExtractVariables policy only considered the portion of the path before the separator, effectively discarding everything following the semicolon, equals sign, or comma.

This behaviour demonstrates that Apigee treats path parameter separators as non-essential when extracting variables using patterns. While this can simplify certain use cases, it also introduces risks, especially when security decisions rely on the full path being validated. This oversight could allow attackers to manipulate path parameters to bypass validations or access restricted resources.

But... We'll dive deeper into the security implications later on. For now, let’s first talk about path-matching operators, specifically, about MatchesPath.

What About the MatchesPath?

Let's explore how the MatchesPath operator behaves when handling paths with separators.

... [MatchesPath] operator looks at a path as a series of parts. Therefore, if the path is: /animals/cats/wild you can think of the path as consisting of the parts: /animals, /cats, and /wild.

The MatchesPath operator lets you use two wildcard notations: a single asterisk (*) and a double asterisk (**). The single asterisk matches one path element. The double asterisk matches one or many path elements...

Apigee Documentation - Conditions with flow variables

So, the MatchesPath operator evaluates whether the request path matches a specified pattern, and it interprets the path as a sequence of parts. For example, if you use the condition proxy.pathsuffix MatchesPath "/path/*" the goal is to match any single segment immediately following /path/

<Flow name="/path/*">
  <Description/>
  <Request/>
  <Response/>
  <Condition>proxy.pathsuffix MatchesPath "/path/*"</Condition>
</Flow>

Here’s how it should work and what actually is observed:

1️⃣ Request: /path/aaa

  • Expected Result: true
  • Actual Result: true

The path /path/aaa matches the pattern because aaa is a straightforward segment following /path/, which is what the * wildcard is designed to catch. However, complications arise when separators are present:

2️⃣ Request: /path;bbbb/aaa

  • Expected Result: false
  • Actual Result: true

The path /path;bbbb/aaa should not match because the semicolon introduces an additional component to the first part of the path. The intent of /path/* is to match a clean /path/ and anything after, but instead, MatchesPath disregards the semicolon and everything after it and still evaluates it as a match.

What does this mean? Well, it means that requests sent to the target system could retain unexpected path details like ;bbbb. In the case of ExtractVariables, only part of the path is extracted and validated.

Doesn’t sound too dangerous? Next, I’ll dive into real vulnerabilities that were found during security assessments of Apigee proxies in the wild that MatchesPath can introduce and show how seemingly benign behaviour can lead to critical security issues.

Example 1: Misconfigured Flow and RouteRule Handling

Let’s explore a scenario where both MatchesPath and exact equality (==) are used to control API routing, but with different effects due to path parameter separators.

In one production Apigee setup, I found a Flow that used MatchesPath for matching /recommendations and a RouteRule that matched the same path using strict equality (==). Here’s what was configured:

<Flow name="fetchRecommendations">
    <Condition>proxy.pathsuffix MatchesPath "/recommendations"</Condition>
</Flow>
...
<RouteRule name="DigitalLayer">
    <TargetEndpoint>DigitalLayer</TargetEndpoint>
    <Condition>proxy.pathsuffix == "/recommendations"</Condition>
</RouteRule>
<RouteRule name="k8smicroservice">
    <TargetEndpoint>k8smicroservice</TargetEndpoint>
</RouteRule>

The issue? While /recommendations was routed to the correct endpoint, a URL with /recommendations;other-info triggered the Flow condition but not the RouteRule.  Consequently, the request was directed to the fallback k8smicroservice endpoint. The major problem? The second backend accepted paths with the pattern /*, treating the request as valid and unintentionally exposing data from a different user!

Example 2: Bypassing Access Control in JavaScript

In this scenario, I encountered an Apigee proxy configuration where a JavaScript-based function performed access control by using regex to block user access to specific paths. This setup had the validateAuthentication function run in the PreFlow, filtering requests before they reached specific flows. These flows, however, used conditional checks like proxy.pathsuffix MatchesPath "/adult-content-lock"

<PreFlow name="PreFlow">
    <Request>
        ...
        <Step>JavaScript.validateAuthentication</Step>
        ...
    </Request>
</PreFlow>
...
<Flows>
    <Flow name="adultContentLock">
        ...
        <Condition>proxy.pathsuffix MatchesPath "/adult-content-lock"</Condition>
    </Flow>
    ...
</Flows>

Here’s a closer look at the JavaScript file where validateAuthentication is defined:

function validateAuthentication () {
  var pathSuffix = context.getVariable("proxy.pathsuffix");
  var accessContext = context.getVariable("accesstoken.context"); // get context to determine if user or system calling our endpoint
  var blockRegexArray = [
    new RegExp("^/adult-content-lock$"),
    new RegExp("^/contracts$"),
    "..."
  ];
  if (accessContext === "USER") { // if user calling
    for (i = 0; i < blockRegexArray.length; i++) {
      // we match with each regex to determine if path should not be accessed by user
      var match = blockRegexArray[i].test(pathSuffix);
      if (match) {
        // throw an error and stop flow execution
        throw new Error("Access denied");
      }
    }
  }
}

The function first checks if the request is coming from a "USER" (as defined by accessContext). It then iterates over blockRegexArray, where each entry is a regex pattern targeting sensitive paths (like /adult-content-lock or /contracts). If any regex pattern matches pathSuffix, an error is thrown to prevent further execution.

Because MatchesPath allowed /adult-content-lock;extra as valid, while the regex in validateAuthentication expected an exact match (due to $ at the end), appending a path separator bypassed the JavaScript validation in PreFlow. This bypass let unauthorised requests reach sensitive flows, creating a critical security vulnerability in the routing setup.

Example 3: Faulty Validation

In this example, another critical oversight stems from the use of path separators in routing and validation. Here, the validation logic is applied in the PreFlow through a JavaScript policy, and it checks the request path for specific matches before allowing it to proceed. The JavaScript function matches the proxy.pathsuffix to predefined paths for sensitive resources, with the expectation of exact matches.

var path = context.getVariable("proxy.pathsuffix");
if ((path === "/available-offers" || path === "/current-offers") && requestVerb === "GET") {
  var accountNumberRegex = new RegExp(/^10\d{8,10}$/);
  var accountNumber = context.getVariable("request.header.x-account-number");

  if (!accountNumberRegex.test(accountNumber)) {
    throw new Error("Invalid account number");
  }
}

However, the flows themselves use proxy.pathsuffix MatchesPath conditions, like. This conditional allows paths with URL path parameter separators, while the JavaScript regex expects exact strings. 

<PreFlow name="PreFlow">
    <Request>
        ...
        <Step>JavaScript.ValidateRequestParameters</Step>
        ...
    </Request>
</PreFlow>
...
<Flows>
    <Flow name="getCurrentOffers">
        ...
        <Condition>proxy.pathsuffix MatchesPath "/current-offers" and request.verb == "GET"</Condition>
    </Flow>
    ...
</Flows>

Due to this inconsistency, adding a separator and extra data to the path bypasses the JavaScript-based check, effectively letting invalidated requests reach target endpoints.

Example 4: AWS as a Target System

In one Apigee configuration, a proxy had an AWS-hosted target system, with paths forwarded directly from Apigee to AWS. Apigee matched paths using MatchesPath, allowing path segments like /service;extra to pass through. While Apigee saw /service;extra as a match, AWS didn’t, sending the request to a default handler instead of /service endpoint.

The crucial problem? Apigee authenticated with JWT token in the Authorization header, but AWS, expecting AWS Signature Version 4 authentication, returned a JSON error that included the entire JWT! This error message exposed sensitive JWT tokens to the client, enabling direct access to AWS services without Apigee mediation, creating a significant security vulnerability.

Conclusion and Recommendations

The primary risk with Apigee’s MatchesPath stems from a common misunderstanding of how it actually handles path separators. In my experience, this pattern of risky MatchesPath usage appears in nearly every other production proxy - and, notably, even in Apigee’s own examples.

sad but true

This widespread reliance on MatchesPath with static conditions highlights the need for more secure configuration standards across Apigee APIs.

So, how can developers better secure their configurations?

Use of Precise Matching Approaches

Whenever possible, prefer strict equality (==) for static paths to eliminate ambiguity. Alternatively, leverage carefully crafted regular expressions to ensure that paths are evaluated precisely, particularly in cases involving sensitive endpoints or data access.

Sandbox Testing

Conduct path manipulation testing in non-production environments to spot potential mismatches before deployment. This practice will help identify configurations that are vulnerable to unexpected path interpretation, enabling more secure configurations to be applied in production environments.

Leverage Tools for Detection

Tools like CodeSent can detect when MatchesPath is applied to static paths, helping to quickly identify and fix vulnerabilities, allowing teams to secure configurations by catching potential problems early on!

Request a Demo

Dealing with Path Parameters
CodeSent, Nikita Markevich 29 October 2024
Share this post
Archive
Beyond the Basics
5 More Steps to Strengthen Your Apigee Security