HTTP Parameter Pollution (HPP) and SSRF

HTTP parameter pollution

Description

Concatenating user input into a query string can introduce delimiters or duplicate parameters, causing HTTP Parameter Pollution (HPP). The request's meaning depends on how the recipient interprets those parameters.

Server-Side Request Forgery (SSRF) is a separate problem in which an attacker controls a server's request destination. Changing the query of a URL with a fixed host does not itself change that destination, but it can lead to SSRF if the receiving API uses the parameter as the URL for another request.

Potential impact

  • An attacker may override existing parameters and trigger unauthorized actions.
  • If this leads to SSRF, internal APIs or cloud metadata services (http://169.254.169.254/) may become accessible.
  • Sensitive information on the internal network may then be exposed.

Remediation

  • Do not concatenate raw user input into URLs or their parameters.
  • Use a lookup table when only predefined values are allowed.
  • Encode query parameter values appropriately with java.net.URLEncoder.encode.
    • Use an allow-list for a fixed set of choices. Do not decode a value and concatenate it unescaped into another URL. Encoding alone does not establish that a destination is trusted.
  • Do not allow arbitrary full request URLs supplied by users.

Examples

Before

java
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.HttpClients;

public class UnsafeHttpRequestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String userInput = request.getParameter("url"); // Use user input
        // Unsafe URL concatenation (example input: 1&url=169.254.169.254)
        String targetUrl = "https://example.com/api?url=api.example.com&query=" + userInput;

        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpGet httpGet = new HttpGet(targetUrl);
            try (CloseableHttpResponse clientResponse = httpClient.execute(httpGet)) {
                // Process the response
            }
        }
    }
}

After

java
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.HttpClients;

public class SafeHttpRequestServlet extends HttpServlet {
    private static final Map<String, String> allowedValues = new HashMap<>();

    static {
        allowedValues.put("item1", "value1");
        allowedValues.put("item2", "value2");
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String userInput = request.getParameter("key");
        if (userInput == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        // Pass only an allowed value from the lookup table
        String safeValue = allowedValues.getOrDefault(userInput, "value1");

        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpGet httpGet = new HttpGet("https://example.com/api?query=" + safeValue);
            try (CloseableHttpResponse clientResponse = httpClient.execute(httpGet)) {
                // Process the response
            }
        }

        // Alternative: encode the query value only. Do not concatenate the decoded value into another URL.
        String encodedString = java.net.URLEncoder.encode(userInput, StandardCharsets.UTF_8);
        final HttpGet httpget = new HttpGet("https://example.com/getId?key=" + encodedString);

    }
}

Explanation:

  • Before: An input containing &url=... can add a duplicate parameter. Whether this becomes SSRF depends on whether the receiving API uses that value as a request destination.
  • After: The request uses a value from an allow-list. The separate example that follows demonstrates query-value encoding and does not execute a request.

Related CVEs

References