This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/9.0.x by this push:
     new 894158fd49 Improve security of Unix domain sockets - APR/Native
894158fd49 is described below

commit 894158fd49679a6bd48d9689905829e1fdc45cb2
Author: Mark Thomas <[email protected]>
AuthorDate: Wed Aug 26 11:22:40 2026 +0100

    Improve security of Unix domain sockets - APR/Native
---
 java/org/apache/tomcat/util/net/AprEndpoint.java | 179 ++++++++++++++++++++++-
 webapps/docs/changelog.xml                       |   7 +
 webapps/docs/config/http.xml                     |  49 +++++++
 3 files changed, 232 insertions(+), 3 deletions(-)

diff --git a/java/org/apache/tomcat/util/net/AprEndpoint.java 
b/java/org/apache/tomcat/util/net/AprEndpoint.java
index 2cad59753a..514ad78f6e 100644
--- a/java/org/apache/tomcat/util/net/AprEndpoint.java
+++ b/java/org/apache/tomcat/util/net/AprEndpoint.java
@@ -24,8 +24,10 @@ import java.nio.ByteBuffer;
 import java.nio.channels.CompletionHandler;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
+import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.FileAttribute;
 import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermissions;
@@ -310,6 +312,93 @@ public class AprEndpoint extends 
AbstractEndpoint<Long,Long> implements SNICallB
     }
 
 
+    /**
+     * Permissions which will be checked for / set on the parent directory of 
the Unix Domain Socket path before the
+     * Unix Domain Socket is created.
+     */
+    private String unixDomainSocketParentPermissions = null;
+
+    /**
+     * Sets the permissions that will be checked for / set on the parent 
directory of the Unix Domain Socket path
+     * before the Unix Domain Socket is created.
+     *
+     * @param unixDomainSocketParentPermissions the permissions string
+     */
+    public void setUnixDomainSocketParentPermissions(String 
unixDomainSocketParentPermissions) {
+        this.unixDomainSocketParentPermissions = 
unixDomainSocketParentPermissions;
+    }
+
+    /**
+     * Returns the permissions that will be checked for / set on the parent 
directory of the Unix Domain Socket path
+     * before the Unix Domain Socket is created.
+     *
+     * @return the permissions string
+     */
+    public String getUnixDomainSocketParentPermissions() {
+        return unixDomainSocketParentPermissions;
+    }
+
+    private Set<PosixFilePermission> 
getUnixDomainSocketParentPermissionsInternal() {
+        String permissionString = getUnixDomainSocketParentPermissions();
+        boolean derivePermissions = false;
+
+        if (permissionString == null) {
+            permissionString = getUnixDomainSocketPathPermissions();
+            /*
+             * UDS permissions will be file permissions which will be missing 
the execute bit required at the directory
+             * level and may include write permissions which are not required 
so set flag to derive a sensible default.
+             */
+            derivePermissions = true;
+        }
+
+        if (permissionString == null || permissionString.length() == 0) {
+            return null;
+        }
+
+        Set<PosixFilePermission> result = 
PosixFilePermissions.fromString(permissionString);
+        if (derivePermissions) {
+            // Add execute before removing write
+            if (result.contains(PosixFilePermission.OWNER_READ) || 
result.contains(PosixFilePermission.OWNER_WRITE)) {
+                result.add(PosixFilePermission.OWNER_EXECUTE);
+            }
+            if (result.contains(PosixFilePermission.GROUP_READ) || 
result.contains(PosixFilePermission.GROUP_WRITE)) {
+                result.add(PosixFilePermission.GROUP_EXECUTE);
+            }
+            if (result.contains(PosixFilePermission.OTHERS_READ) || 
result.contains(PosixFilePermission.OTHERS_WRITE)) {
+                result.add(PosixFilePermission.OTHERS_EXECUTE);
+            }
+            result.remove(PosixFilePermission.GROUP_WRITE);
+            result.remove(PosixFilePermission.OTHERS_WRITE);
+        }
+
+        return result;
+    }
+
+
+    /**
+     * If the parent directory for the UDS already exists, the name of the 
expected owner.
+     */
+    private String unixDomainSocketParentOwner = null;
+
+    /**
+     * Returns the expected name of the owner of the parent directory for the 
UDS if it already exists.
+     *
+     * @return the owner name
+     */
+    public String getUnixDomainSocketParentOwner() {
+        return unixDomainSocketParentOwner;
+    }
+
+    /**
+     * Sets the expected name of the owner of the parent directory for the UDS 
if it already exists.
+     *
+     * @param unixDomainSocketParentOwner the expected owner name
+     */
+    public void setUnixDomainSocketParentOwner(String 
unixDomainSocketParentOwner) {
+        this.unixDomainSocketParentOwner = unixDomainSocketParentOwner;
+    }
+
+
     // --------------------------------------------------------- Public Methods
 
     /**
@@ -380,6 +469,73 @@ public class AprEndpoint extends 
AbstractEndpoint<Long,Long> implements SNICallB
             } else {
                 throw new 
Exception(sm.getString("endpoint.init.unixnotavail"));
             }
+
+            /*
+             * If permissions are configured, need to create the parent folder 
first with the correct permissions to
+             * avoid a TOCTOU issue with the Unix Domain Socket.
+             */
+            Set<PosixFilePermission> parentPosixPermissions = 
getUnixDomainSocketParentPermissionsInternal();
+            if (parentPosixPermissions != null) {
+                Path udsPath = 
Paths.get(getUnixDomainSocketPath()).toAbsolutePath();
+                Path parentPath = udsPath.getParent();
+                java.io.File parentFile = parentPath.toFile();
+
+                boolean posixSupported = 
parentPath.getFileSystem().supportedFileAttributeViews().contains("posix");
+
+                if (!Files.exists(parentPath, LinkOption.NOFOLLOW_LINKS) || 
!Files.readAttributes(
+                        parentPath, BasicFileAttributes.class, 
LinkOption.NOFOLLOW_LINKS).isDirectory()) {
+                    if (posixSupported) {
+                        /*
+                         * The immediate parent will be configured with the 
requested permissions. If other parents are
+                         * created, they will be created with the specified 
permissions masked as per the current
+                         * process's UMASK.
+                         */
+                        Files.createDirectories(parentPath, 
PosixFilePermissions.asFileAttribute(parentPosixPermissions));
+                        FileAttribute<Set<PosixFilePermission>> attrs = 
PosixFilePermissions.asFileAttribute(parentPosixPermissions);
+                        Files.setAttribute(parentPath, attrs.name(), 
attrs.value());
+                    } else {
+                        Files.createDirectories(parentPath);
+                    }
+                }
+
+                // Directory should exist now
+                if (posixSupported) {
+                    // Check permissions are at least as restrictive as those 
specified.
+                    Set<PosixFilePermission> currentPosixPermissions =
+                            Files.getPosixFilePermissions(parentPath, 
LinkOption.NOFOLLOW_LINKS);
+                    currentPosixPermissions.removeAll(parentPosixPermissions);
+                    if (!currentPosixPermissions.isEmpty()) {
+                        throw new 
IllegalStateException(sm.getString("endpoint.nio.uds.parentLaxPermissions",
+                                parentPath, currentPosixPermissions));
+                    }
+                } else {
+                    log.warn(sm.getString("endpoint.nio.uds.parentNotPosix", 
parentPath));
+                    setPermissionsForNonPosixFile(parentFile, 
parentPosixPermissions);
+                }
+
+                /*
+                 * Check the owner - should either be the current user (if 
created above) or the expected owner (if
+                 * pre-created).
+                 */
+                String expectedOwner = getUnixDomainSocketParentOwner();
+                if (expectedOwner == null || !expectedOwner.isEmpty()) {
+                    String owner;
+                    try {
+                        owner = Files.getOwner(parentPath, 
LinkOption.NOFOLLOW_LINKS).getName();
+                    } catch (Throwable t) {
+                        ExceptionUtils.handleThrowable(t);
+                        throw new 
IllegalStateException(sm.getString("endpoint.nio.uds.noOwner", parentPath), t);
+                    }
+                    if (!owner.equals(expectedOwner)) {
+                        String currentUser = System.getProperty("user.name");
+                        if (!owner.equals(currentUser)) {
+                            throw new 
IllegalStateException(sm.getString("endpoint.nio.uds.parentOwner",
+                                    parentPath, owner, expectedOwner, 
currentUser));
+
+                        }
+                    }
+                }
+            }
         } else {
 
             if (getAddress() != null) {
@@ -424,10 +580,17 @@ public class AprEndpoint extends 
AbstractEndpoint<Long,Long> implements SNICallB
 
         if (family == Socket.APR_UNIX) {
             if (getUnixDomainSocketPathPermissions() != null) {
-                FileAttribute<Set<PosixFilePermission>> attrs = 
PosixFilePermissions
-                        
.asFileAttribute(PosixFilePermissions.fromString(getUnixDomainSocketPathPermissions()));
                 Path path = Paths.get(getUnixDomainSocketPath());
-                Files.setAttribute(path, attrs.name(), attrs.value());
+                Set<PosixFilePermission> permissions =
+                        
PosixFilePermissions.fromString(getUnixDomainSocketPathPermissions());
+                if 
(path.getFileSystem().supportedFileAttributeViews().contains("posix")) {
+                    FileAttribute<Set<PosixFilePermission>> attrs = 
PosixFilePermissions.asFileAttribute(permissions);
+                    Files.setAttribute(path, attrs.name(), attrs.value());
+                } else {
+                    log.warn(sm.getString("endpoint.nio.uds.notPosix", path));
+                    java.io.File file = path.toFile();
+                    setPermissionsForNonPosixFile(file, permissions);
+                }
             }
         } else {
             if (OS.IS_WIN32 || OS.IS_WIN64) {
@@ -476,6 +639,16 @@ public class AprEndpoint extends 
AbstractEndpoint<Long,Long> implements SNICallB
     }
 
 
+    private void setPermissionsForNonPosixFile(java.io.File file, 
Set<PosixFilePermission> permissions) {
+        if (permissions.contains(PosixFilePermission.OTHERS_READ) && 
!file.setReadable(true, false)) {
+            log.warn(sm.getString("endpoint.nio.perms.readFail", 
file.getAbsolutePath()));
+        }
+        if (permissions.contains(PosixFilePermission.OTHERS_WRITE) && 
!file.setWritable(true, false)) {
+            log.warn(sm.getString("endpoint.nio.perms.writeFail", 
file.getAbsolutePath()));
+        }
+    }
+
+
     @Override
     protected void createSSLContext(SSLHostConfig sslHostConfig) throws 
Exception {
         OpenSSLContext sslContext = null;
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 90bb094137..97cca2b243 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -201,6 +201,13 @@
         Align cipher order with the results produced by OpenSSL if the cipher
         specification adds, removes and then re-adds ciphers. (markt)
       </fix>
+      <add>
+        Add new attributes (<code>unixDomainSocketParentPermissions</code> and
+        <code>unixDomainSocketParentOwner</code>) to the APR/Native connector 
to
+        provide additional control over the security of Unix Domain Sockets.
+        Additional checks (enabled by default) have also been added for the
+        directory where the Unix Domain Socket will be created.(markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Jasper">
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index bdcf7c994b..10b3fa54f2 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -1270,6 +1270,55 @@
         the response.</p>
       </attribute>
 
+      <attribute name="unixDomainSocketPath" required="false">
+        <p>Where supported, the path to a Unix Domain Socket that this
+        <strong>Connector</strong> will create and await incoming connections.
+        When this is specified, the otherwise mandatory <code>port</code>
+        attribute may be omitted.</p>
+      </attribute>
+
+      <attribute name="unixDomainSocketPathPermissions" required="false">
+        <p>Where supported, the posix permissions that will be applied to the
+        Unix Domain Socket specified with <code>unixDomainSocketPath</code>
+        above.  If the Unix Domain Socket is required to be created securely,
+        these permissions should be selected as to limit access to trusted
+        users.</p>
+        <p>The permissions are specified as a string of nine characters, in
+        three sets of three: (r)ead, (w)rite and e(x)ecute for owner, group and
+        others respectively. If a permission is not granted, a hyphen is
+        used.</p>
+        <p>If unspecified, no permissions are explicitly set meaning that the
+        resulting permissions will be determined by the current umask.</p>
+      </attribute>
+
+      <attribute name="unixDomainSocketParentOwner" required="false">
+        <p>The expected ownership of the directory where the Unix Domain Socket
+        will be created. The directory must be owned either by this user or the
+        current user associated with the Tomcat process.</p>
+        <p>This attribute is ignored if
+        <code>unixDomainSocketParentPermissions</code> are skipped. If not
+        ignored, the ownership checks on the parent directory, can be skipped 
by
+        setting this attribute to the empty string.</p>
+        <p>If unspecified, the default value is unset.</p>
+      </attribute>
+
+      <attribute name="unixDomainSocketParentPermissions" required="false">
+        <p>Where supported, the posix permissions that will be checked on /
+        applied to the directory where the Unix Domain Socket will be created.
+        If the Unix Domain Socket is required to be created securely, these
+        permissions should be selected as to limit access to trusted users.</p>
+        <p>To skip the permission checks on the parent directory, set this
+        attribute to the empty string.</p>
+        <p>The permissions are specified as a string of nine characters, in
+        three sets of three: (r)ead, (w)rite and e(x)ecute for owner, group and
+        others respectively. If a permission is not granted, a hyphen is used.
+        If unspecified, the value of
+        <code>unixDomainSocketPathPermissions</code> is used with the addition
+        of the appropriate execute permission for every read and/or write
+        permission that is specified and the removal of group and other write
+        permissions if present.</p>
+      </attribute>
+
     </attributes>
 
   </subsection>


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to