Browse Source

fix issue #688 - photo upload/download mime type enforcement

Jason Rivard 2 years ago
parent
commit
19e19c1b08

+ 2 - 0
server/src/main/java/password/pwm/AppProperty.java

@@ -340,6 +340,8 @@ public enum AppProperty
     SECURITY_HTTP_PERFORM_CSRF_HEADER_CHECKS        ( "security.http.performCsrfHeaderChecks" ),
     SECURITY_HTTP_PROMISCUOUS_ENABLE                ( "security.http.promiscuousEnable" ),
     SECURITY_HTTP_CONFIG_CSP_HEADER                 ( "security.http.config.cspHeader" ),
+    SECURITY_HTTP_USER_PHOTO_MIME_TYPES             ( "security.http.permittedUserPhotoMimeTypes" ),
+    SECURITY_HTTP_PERMITTED_URL_PATH_CHARS          ( "security.http.permittedUrlPathCharacters" ),
     SECURITY_HTTPSSERVER_SELF_FUTURESECONDS         ( "security.httpsServer.selfCert.futureSeconds" ),
     SECURITY_HTTPSSERVER_SELF_ALG                   ( "security.httpsServer.selfCert.alg" ),
     SECURITY_HTTPSSERVER_SELF_KEY_SIZE              ( "security.httpsServer.selfCert.keySize" ),

+ 6 - 0
server/src/main/java/password/pwm/config/AppConfig.java

@@ -315,6 +315,12 @@ public class AppConfig implements SettingReader
                 && readSettingAsPassword( PwmSetting.DATABASE_PASSWORD ) != null;
     }
 
+    public List<String> permittedPhotoMimeTypes()
+    {
+        final String permittedMimeTypesStr = readAppProperty( AppProperty.SECURITY_HTTP_USER_PHOTO_MIME_TYPES );
+        return List.copyOf( StringUtil.splitAndTrim( permittedMimeTypesStr, "," ) );
+    }
+
     @Override
     public PasswordData readSettingAsPassword( final PwmSetting setting )
     {

+ 0 - 1
server/src/main/java/password/pwm/config/value/FormValue.java

@@ -185,7 +185,6 @@ public class FormValue extends AbstractValue implements StoredValue
             }
             if ( formRow.getType() == FormConfiguration.Type.photo )
             {
-                sb.append( " MimeTypes: " ).append( StringUtil.collectionToString( formRow.getMimeTypes() ) ).append( '\n' );
                 sb.append( " MaxSize: " ).append( formRow.getMaximumSize() ).append( '\n' );
             }
         }

+ 0 - 10
server/src/main/java/password/pwm/config/value/data/FormConfiguration.java

@@ -36,7 +36,6 @@ import password.pwm.util.java.StringUtil;
 
 import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -121,15 +120,6 @@ public class FormConfiguration
     @Builder.Default
     private Map<String, String> selectOptions = Collections.emptyMap();
 
-    @Builder.Default
-    private List<String> mimeTypes = Arrays.asList(
-            "image/gif",
-            "image/png",
-            "image/jpeg",
-            "image/bmp",
-            "image/webp"
-    );
-
     @Builder.Default
     private int maximumSize = 65000;
 

+ 6 - 0
server/src/main/java/password/pwm/http/PwmURL.java

@@ -426,6 +426,12 @@ public class PwmURL
         return "";
     }
 
+    public List<String> getPathSegments()
+    {
+        final String uriPath = uri.getPath();
+        return StringUtil.splitAndTrim( uriPath, "/" );
+    }
+
     public String determinePwmServletPath( )
     {
         final String requestPath = this.pathMinusContextAndDomain();

+ 23 - 0
server/src/main/java/password/pwm/http/ServletUtility.java

@@ -21,13 +21,18 @@
 package password.pwm.http;
 
 import password.pwm.PwmConstants;
+import password.pwm.config.AppConfig;
+import password.pwm.data.ImmutableByteArray;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.StringUtil;
 
 import javax.servlet.http.HttpServletRequest;
 import java.io.IOException;
+import java.net.URLConnection;
+import java.util.List;
 
 public final class ServletUtility
 {
@@ -48,4 +53,22 @@ public final class ServletUtility
         }
         return value;
     }
+
+
+    public static String mimeTypeForUserPhoto(
+            final AppConfig configuration,
+            final ImmutableByteArray immutableByteArray
+    )
+            throws IOException, PwmUnrecoverableException
+    {
+        final List<String> permittedMimeTypes = configuration.permittedPhotoMimeTypes();
+
+        final String mimeType = URLConnection.guessContentTypeFromStream( immutableByteArray.newByteArrayInputStream() );
+        if ( !StringUtil.isEmpty( mimeType ) && permittedMimeTypes.contains( mimeType ) )
+        {
+            return mimeType;
+        }
+        final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_FILE_TYPE_INCORRECT, "unsupported mime type" );
+        throw new PwmUnrecoverableException( errorInformation );
+    }
 }

+ 29 - 0
server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

@@ -72,6 +72,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 public class RequestInitializationFilter implements Filter
@@ -488,6 +489,31 @@ public class RequestInitializationFilter implements Filter
         }
     }
 
+    private static void checkURlPathSegments( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException
+    {
+        if ( pwmRequest.getURL().isResourceURL() )
+        {
+            return;
+        }
+
+        final String checkRegexPatternString = pwmRequest.getAppConfig().readAppProperty( AppProperty.SECURITY_HTTP_PERMITTED_URL_PATH_CHARS );
+        if ( StringUtil.isEmpty( checkRegexPatternString ) )
+        {
+            return;
+        }
+
+        final Pattern pattern = Pattern.compile( checkRegexPatternString );
+        for ( final String pathPart : pwmRequest.getURL().getPathSegments() )
+        {
+            if ( !pattern.matcher( pathPart ).matches() )
+            {
+                final String errorMsg = "request URL path segment contains illegal characters";
+                final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SECURITY_VIOLATION, errorMsg );
+                throw new PwmUnrecoverableException( errorInformation );
+            }
+        }
+    }
 
     private static void handleRequestInitialization(
             final PwmRequest pwmRequest
@@ -554,6 +580,9 @@ public class RequestInitializationFilter implements Filter
         // check the user's IP address
         checkIfSourceAddressChanged( pwmRequest );
 
+        // check url path segments
+        checkURlPathSegments( pwmRequest );
+
         // check total time.
         checkTotalSessionTime( pwmRequest );
 

+ 26 - 26
server/src/main/java/password/pwm/http/servlet/updateprofile/UpdateProfileServlet.java

@@ -31,6 +31,7 @@ import password.pwm.bean.TokenDestinationItem;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.profile.UpdateProfileProfile;
 import password.pwm.config.value.data.FormConfiguration;
+import password.pwm.data.ImmutableByteArray;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmDataValidationException;
 import password.pwm.error.PwmError;
@@ -43,7 +44,7 @@ import password.pwm.http.ProcessStatus;
 import password.pwm.http.PwmRequest;
 import password.pwm.http.PwmRequestAttribute;
 import password.pwm.http.PwmSession;
-import password.pwm.data.ImmutableByteArray;
+import password.pwm.http.ServletUtility;
 import password.pwm.http.bean.UpdateProfileBean;
 import password.pwm.http.servlet.ControlledPwmServlet;
 import password.pwm.i18n.Message;
@@ -55,11 +56,10 @@ import password.pwm.svc.token.TokenService;
 import password.pwm.svc.token.TokenType;
 import password.pwm.svc.token.TokenUtil;
 import password.pwm.util.form.FormUtility;
-import password.pwm.util.java.CollectionUtil;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.PwmUtil;
-import password.pwm.util.json.JsonFactory;
 import password.pwm.util.java.StringUtil;
+import password.pwm.util.json.JsonFactory;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroRequest;
 import password.pwm.ws.server.RestResultBean;
@@ -68,11 +68,9 @@ import javax.servlet.ServletException;
 import javax.servlet.annotation.WebServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.URLConnection;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -521,27 +519,27 @@ public class UpdateProfileServlet extends ControlledPwmServlet
             final Optional<InputStream> uploadedFile = pwmRequest.readFileUploadStream( PwmConstants.PARAM_FILE_UPLOAD );
             if ( uploadedFile.isPresent() )
             {
-                try ( InputStream inputStream = uploadedFile.get() )
+
+                final ImmutableByteArray bytes = JavaHelper.copyToBytes( uploadedFile.get(), maxSize + 1 );
+
+                if ( bytes.size() > maxSize )
                 {
-                    final ImmutableByteArray bytes = JavaHelper.copyToBytes( inputStream, maxSize );
-                    final String b64String = StringUtil.base64Encode( bytes.copyOf() );
-
-                    if ( !CollectionUtil.isEmpty( formConfiguration.getMimeTypes() ) )
-                    {
-                        final String mimeType = URLConnection.guessContentTypeFromStream( bytes.newByteArrayInputStream() );
-                        if ( !formConfiguration.getMimeTypes().contains( mimeType ) )
-                        {
-                            final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_FILE_TYPE_INCORRECT, "incorrect file type of " + mimeType, new String[]
-                                    {
-                                            mimeType,
-                                    }
-                            );
-                            pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
-                            return ProcessStatus.Halt;
-                        }
-                    }
-                    updateProfileBean.getFormData().put( fieldName, b64String );
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_FILE_TOO_LARGE,
+                            "file size exceeds maximum file size (" + maxSize + ")" );
+                    pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
+                    return ProcessStatus.Halt;
                 }
+
+                if ( ServletUtility.mimeTypeForUserPhoto( pwmRequest.getAppConfig(), bytes ).isEmpty() )
+                {
+                    final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_FILE_TYPE_INCORRECT,
+                            "unsupported mime type" );
+                    pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
+                    return ProcessStatus.Halt;
+                }
+
+                final String b64String = StringUtil.base64Encode( bytes.copyOf() );
+                updateProfileBean.getFormData().put( fieldName, b64String );
             }
         }
 
@@ -563,7 +561,8 @@ public class UpdateProfileServlet extends ControlledPwmServlet
     }
 
     @ActionHandler( action = "readPhoto" )
-    public ProcessStatus readPhotoHandler( final PwmRequest pwmRequest ) throws ServletException, PwmUnrecoverableException, IOException
+    public ProcessStatus readPhotoHandler( final PwmRequest pwmRequest )
+            throws PwmUnrecoverableException, IOException
     {
         final String fieldName = pwmRequest.readParameterAsString( "field" );
         final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
@@ -573,10 +572,11 @@ public class UpdateProfileServlet extends ControlledPwmServlet
         {
             final byte[] bytes = StringUtil.base64Decode( b64value );
 
+            final String mimeType = ServletUtility.mimeTypeForUserPhoto( pwmRequest.getAppConfig(), ImmutableByteArray.of( bytes ) );
+
             try ( OutputStream outputStream = pwmRequest.getPwmResponse().getOutputStream() )
             {
                 final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
-                final String mimeType = URLConnection.guessContentTypeFromStream( new ByteArrayInputStream( bytes ) );
                 resp.setContentType( mimeType );
                 outputStream.write( bytes );
                 outputStream.flush();

+ 5 - 6
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -50,6 +50,7 @@ import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmOperationalException;
 import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.http.ServletUtility;
 import password.pwm.svc.cache.CacheKey;
 import password.pwm.svc.cache.CachePolicy;
 import password.pwm.svc.stats.EpsStatistic;
@@ -64,9 +65,7 @@ import password.pwm.util.macro.MacroRequest;
 import password.pwm.util.secure.PwmTrustManager;
 
 import javax.net.ssl.X509TrustManager;
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.net.URLConnection;
 import java.security.cert.X509Certificate;
 import java.time.Instant;
 import java.util.Arrays;
@@ -782,7 +781,7 @@ public class LdapOperationsHelper
             throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "ldap photo attribute is not configured" ) );
         }
 
-        final byte[] photoData;
+        final ImmutableByteArray photoData;
         final String mimeType;
         try
         {
@@ -792,8 +791,8 @@ public class LdapOperationsHelper
             {
                 throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "user has no photo data stored in LDAP attribute" ) );
             }
-            photoData = photoAttributeData[ 0 ];
-            mimeType = URLConnection.guessContentTypeFromStream( new ByteArrayInputStream( photoData ) );
+            photoData = ImmutableByteArray.of( photoAttributeData[ 0 ] );
+            mimeType = ServletUtility.mimeTypeForUserPhoto( domainConfig.getAppConfig(), photoData );
         }
         catch ( final IOException | ChaiOperationException e )
         {
@@ -803,7 +802,7 @@ public class LdapOperationsHelper
         {
             throw PwmUnrecoverableException.fromChaiException( e );
         }
-        return Optional.of( new PhotoDataBean( mimeType, ImmutableByteArray.of( photoData ) ) );
+        return Optional.of( new PhotoDataBean( mimeType, photoData ) );
     }
 
 

+ 2 - 0
server/src/main/resources/password/pwm/AppProperty.properties

@@ -315,6 +315,8 @@ security.http.forceRequestSequencing=false
 security.http.stripHeaderRegex=\\n|\\r|(?ism)%0A|%0D
 security.http.performCsrfHeaderChecks=false
 security.http.promiscuousEnable=false
+security.http.permittedUserPhotoMimeTypes=image/gif,image/png,image/jpeg
+security.http.permittedUrlPathCharacters=^[a-zA-Z0-9-]*$
 security.http.config.cspHeader=default-src 'self'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; report-uri @PwmContextPath@/public/command/cspReport
 security.httpsServer.selfCert.futureSeconds=63113904
 security.httpsServer.selfCert.alg=RSA

+ 1 - 1
webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp

@@ -125,7 +125,7 @@
                 <script type="application/javascript">
                     PWM_GLOBAL['startupFunctions'].push(function(){
                         PWM_MAIN.addEventHandler('button-uploadPhoto-<%=loopConfiguration.getName()%>',"click",function(){
-                            var accept = '<%=StringUtil.collectionToString(loopConfiguration.getMimeTypes())%>';
+                            var accept = '<%=StringUtil.collectionToString(formPwmRequest.getAppConfig().permittedPhotoMimeTypes())%>';
                             PWM_UPDATE.uploadPhoto('<%=loopConfiguration.getName()%>',{accept:accept});
                         });
                     });

+ 1 - 64
webapp/src/main/webapp/public/resources/js/configeditor-settings-form.js

@@ -33,7 +33,6 @@ FormTableHandler.newRowValue = {
     javascript:'',
     regex:'',
     source:'ldap',
-    mimeTypes:['image/gif','image/png','image/jpeg','image/bmp','image/webp'],
     maximumSize:65000
 };
 
@@ -289,7 +288,7 @@ FormTableHandler.showOptionsDialog = function(keyName, iteration) {
         bodyText += '</tr><tr>';
     }
 
-    if ('select' in options) {
+    if (currentValue['type'] === 'select') {
         bodyText += '<td class="key">Select Options</td><td><button class="btn" id="' + inputID + 'editOptionsButton"><span class="btn-icon pwm-icon pwm-icon-list-ul"/> Edit</button></td>';
         bodyText += '</tr>';
     }
@@ -303,9 +302,6 @@ FormTableHandler.showOptionsDialog = function(keyName, iteration) {
     }
 
     if (currentValue['type'] === 'photo') {
-        bodyText += '<td class="key">MimeTypes</td><td><button class="btn" id="' + inputID + 'editMimeTypesButton"><span class="btn-icon pwm-icon pwm-icon-list-ul"/> Edit</button></td>';
-        bodyText += '</tr>';
-
         bodyText += '<td class="key">Maximum Size (bytes)</td><td><input min="0" pattern="[0-9]{1,10}" max="10000000" style="width: 90px" type="number" id="' + inputID + 'maximumSize' + '"/></td>';
         bodyText += '</tr><tr>';
     }
@@ -320,10 +316,6 @@ FormTableHandler.showOptionsDialog = function(keyName, iteration) {
             FormTableHandler.showSelectOptionsDialog(keyName,iteration);
         });
 
-        PWM_MAIN.addEventHandler(inputID + 'editMimeTypesButton', 'click', function(){
-            FormTableHandler.showMimeTypesDialog(keyName,iteration);
-        });
-
         PWM_MAIN.addEventHandler(inputID + 'description','click',function(){
             FormTableHandler.showDescriptionDialog(keyName, iteration);
         });
@@ -582,58 +574,3 @@ FormTableHandler.showDescriptionDialog = function(keyName, iteration) {
     const title = 'Description for ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'];
     FormTableHandler.multiLocaleStringDialog(keyName, iteration, 'description', finishAction, title);
 };
-
-FormTableHandler.showMimeTypesDialog = function(keyName, iteration) {
-    const inputID = 'value_' + keyName + '_' + iteration + "_" + "selectOptions_";
-    let bodyText = '';
-    bodyText += '<table class="noborder" id="' + inputID + 'table"">';
-    bodyText += '<tr>';
-    bodyText += '</tr><tr>';
-    for (const optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['mimeTypes']) {
-        (function(optionName) {
-            const value = PWM_VAR['clientSettingCache'][keyName][iteration]['mimeTypes'][optionName];
-            const optionID = inputID + optionName;
-            bodyText += '<td><div class="noWrapTextBox">' + value + '</div></td>';
-            bodyText += '<td class="noborder" style="">';
-            bodyText += '<span id="' + optionID + '-removeButton" class="delete-row-icon action-icon pwm-icon pwm-icon-times"></span>';
-            bodyText += '</td>';
-            bodyText += '</tr><tr>';
-        }(optionName));
-    }
-    bodyText += '</tr></table>';
-    bodyText += '<br/><br/><br/>';
-    bodyText += '<input class="configStringInput" pattern=".*/.*" style="width:200px" type="text" placeholder="Value" required id="addValue"/>';
-    bodyText += '<button id="addItemButton"><span class="btn-icon pwm-icon pwm-icon-plus-square"/> Add</button>';
-
-    PWM_MAIN.showDialog({
-        title: 'Mime Types - ' + PWM_VAR['clientSettingCache'][keyName][iteration]['name'],
-        text: bodyText,
-        okAction: function(){
-            FormTableHandler.showOptionsDialog(keyName,iteration);
-        }
-    });
-
-    for (const optionName in PWM_VAR['clientSettingCache'][keyName][iteration]['mimeTypes']) {
-        (function(optionName) {
-            const optionID = inputID + optionName;
-            PWM_MAIN.addEventHandler(optionID + '-removeButton','click',function(){
-                delete PWM_VAR['clientSettingCache'][keyName][iteration]['mimeTypes'][optionName];
-                FormTableHandler.write(keyName);
-                FormTableHandler.showMimeTypesDialog(keyName, iteration);
-            });
-        }(optionName));
-    }
-
-    PWM_MAIN.addEventHandler('addItemButton','click',function(){
-        const value = PWM_MAIN.getObject('addValue').value;
-
-        if (value === null || value.length < 1) {
-            alert('Value field is required');
-            return;
-        }
-
-        PWM_VAR['clientSettingCache'][keyName][iteration]['mimeTypes'].push(value);
-        FormTableHandler.write(keyName);
-        FormTableHandler.showMimeTypesDialog(keyName, iteration);
-    });
-};