From 64fa5b99442589ef0bf2a7fcd71ad2bc68b35fad Mon Sep 17 00:00:00 2001
From: Mark Thomas <markt@apache.org>
Date: Tue, 21 Jan 2020 15:04:12 +0000
Subject: [PATCH] Add new AJP attribute allowedArbitraryRequestAttributes

Requests with unrecognised attributes will be blocked with a 403
---
 .../coyote/ajp/AbstractAjpProtocol.java       | 13 +++++++
 java/org/apache/coyote/ajp/AjpProcessor.java  | 36 ++++++++++++++++++-
 webapps/docs/config/ajp.xml                   | 19 ++++++++++
 4 files changed, 72 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/coyote/ajp/AbstractAjpProtocol.java b/java/org/apache/coyote/ajp/AbstractAjpProtocol.java
index 81da7da6d1..a2f5e2844a 100644
--- a/java/org/apache/coyote/ajp/AbstractAjpProtocol.java
+++ b/java/org/apache/coyote/ajp/AbstractAjpProtocol.java
@@ -17,6 +17,7 @@
 package org.apache.coyote.ajp;
 
 import java.net.InetAddress;
+import java.util.regex.Pattern;
 
 import org.apache.coyote.AbstractProtocol;
 import org.apache.coyote.Processor;
@@ -188,6 +189,18 @@ public boolean getSecretRequired() {
     }
 
 
+    private Pattern allowedArbitraryRequestAttributesPattern;
+    public void setAllowedArbitraryRequestAttributes(String allowedArbitraryRequestAttributes) {
+        this.allowedArbitraryRequestAttributesPattern = Pattern.compile(allowedArbitraryRequestAttributes);
+    }
+    public String getAllowedArbitraryRequestAttributes() {
+        return allowedArbitraryRequestAttributesPattern.pattern();
+    }
+    protected Pattern getAllowedArbitraryRequestAttributesPattern() {
+        return allowedArbitraryRequestAttributesPattern;
+    }
+
+
     /**
      * AJP packet size.
      */
diff --git a/java/org/apache/coyote/ajp/AjpProcessor.java b/java/org/apache/coyote/ajp/AjpProcessor.java
index d466de2c09..f3d783f546 100644
--- a/java/org/apache/coyote/ajp/AjpProcessor.java
+++ b/java/org/apache/coyote/ajp/AjpProcessor.java
@@ -25,6 +25,11 @@
 import java.security.NoSuchProviderException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import javax.servlet.http.HttpServletResponse;
 
@@ -78,6 +83,9 @@
     private static final byte[] pongMessageArray;
 
 
+    private static final Set<String> javaxAttributes;
+
+
     static {
         // Allocate the end message array
         AjpMessage endMessage = new AjpMessage(16);
@@ -118,6 +126,14 @@
         pongMessageArray = new byte[pongMessage.getLen()];
         System.arraycopy(pongMessage.getBuffer(), 0, pongMessageArray,
                 0, pongMessage.getLen());
+
+        // Build the Set of javax attributes
+        Set<String> s = new HashSet<>();
+        s.add("javax.servlet.request.cipher_suite");
+        s.add("javax.servlet.request.key_size");
+        s.add("javax.servlet.request.ssl_session");
+        s.add("javax.servlet.request.X509Certificate");
+        javaxAttributes= Collections.unmodifiableSet(s);
     }
 
 
@@ -728,8 +744,26 @@ private void prepareRequest() {
                     }
                 } else if(n.equals(Constants.SC_A_SSL_PROTOCOL)) {
                     request.setAttribute(SSLSupport.PROTOCOL_VERSION_KEY, v);
+                } else if (n.equals("JK_LB_ACTIVATION")) {
+                    request.setAttribute(n, v);
+                } else if (javaxAttributes.contains(n)) {
+                    request.setAttribute(n, v);
                 } else {
-                    request.setAttribute(n, v );
+                    // All 'known' attributes will be processed by the previous
+                    // blocks. Any remaining attribute is an 'arbitrary' one.
+                    Pattern pattern = protocol.getAllowedArbitraryRequestAttributesPattern();
+                    if (pattern == null) {
+                        response.setStatus(403);
+                        setErrorState(ErrorState.CLOSE_CLEAN, null);
+                    } else {
+                        Matcher m = pattern.matcher(n);
+                        if (m.matches()) {
+                            request.setAttribute(n, v);
+                        } else {
+                            response.setStatus(403);
+                            setErrorState(ErrorState.CLOSE_CLEAN, null);
+                        }
+                    }
                 }
                 break;
 
diff --git a/webapps/docs/config/ajp.xml b/webapps/docs/config/ajp.xml
index 3999a13e66..69348a18e2 100644
--- a/webapps/docs/config/ajp.xml
+++ b/webapps/docs/config/ajp.xml
@@ -311,6 +311,25 @@
       port. By default, the loopback address will be used.</p>
     </attribute>
 
+    <attribute name="allowedArbitraryRequestAttributes" required="false">
+      <p>The AJP protocol passes some information from the reverse proxy to the
+      AJP connector using request attributes. These attributes are:</p>
+      <ul>
+        <li>javax.servlet.request.cipher_suite</li>
+        <li>javax.servlet.request.key_size</li>
+        <li>javax.servlet.request.ssl_session</li>
+        <li>javax.servlet.request.X509Certificate</li>
+        <li>AJP_LOCAL_ADDR</li>
+        <li>AJP_REMOTE_PORT</li>
+        <li>AJP_SSL_PROTOCOL</li>
+        <li>JK_LB_ACTIVATION</li>
+      </ul>
+      <p>The AJP protocol supports the passing of arbitrary request attributes.
+      Requests containing arbitrary request attributes will be rejected with a
+      403 response unless the entire attribute name matches this regular
+      expression. If not specified, the default value is <code>null</code>.</p>
+    </attribute>
+
     <attribute name="bindOnInit" required="false">
       <p>Controls when the socket used by the connector is bound. By default it
       is bound when the connector is initiated and unbound when the connector is
