Skip to content

Commit

Permalink
fix issue #688 - photo upload/download mime type enforcement
Browse files Browse the repository at this point in the history
  • Loading branch information
jrivard committed Feb 10, 2023
1 parent 18a405e commit 19e19c1
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 108 deletions.
2 changes: 2 additions & 0 deletions server/src/main/java/password/pwm/AppProperty.java
Original file line number Diff line number Diff line change
Expand Up @@ -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" ),
Expand Down
6 changes: 6 additions & 0 deletions server/src/main/java/password/pwm/config/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,12 @@ public boolean hasDbConfigured( )
&& 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 )
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ public String toDebugString( final Locale locale )
}
if ( formRow.getType() == FormConfiguration.Type.photo )
{
sb.append( " MimeTypes: " ).append( StringUtil.collectionToString( formRow.getMimeTypes() ) ).append( '\n' );
sb.append( " MaxSize: " ).append( formRow.getMaximumSize() ).append( '\n' );
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -121,15 +120,6 @@ public enum Source
@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;

Expand Down
6 changes: 6 additions & 0 deletions server/src/main/java/password/pwm/http/PwmURL.java
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,12 @@ public String getPostServletPath( final PwmServletDefinition pwmServletDefinitio
return "";
}

public List<String> getPathSegments()
{
final String uriPath = uri.getPath();
return StringUtil.splitAndTrim( uriPath, "/" );
}

public String determinePwmServletPath( )
{
final String requestPath = this.pathMinusContextAndDomain();
Expand Down
23 changes: 23 additions & 0 deletions server/src/main/java/password/pwm/http/ServletUtility.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -48,4 +53,22 @@ public static String readRequestBodyAsString( final HttpServletRequest req, fina
}
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 );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
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
Expand Down Expand Up @@ -488,6 +489,31 @@ public static void addStaticResponseHeaders(
}
}

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
Expand Down Expand Up @@ -554,6 +580,9 @@ private static void handleRequestSecurityChecks( final PwmRequest pwmRequest )
// check the user's IP address
checkIfSourceAddressChanged( pwmRequest );

// check url path segments
checkURlPathSegments( pwmRequest );

// check total time.
checkTotalSessionTime( pwmRequest );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand All @@ -43,7 +44,7 @@
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;
Expand All @@ -55,11 +56,10 @@
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;
Expand All @@ -68,11 +68,9 @@
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;
Expand Down Expand Up @@ -521,27 +519,27 @@ public ProcessStatus uploadPhoto( final PwmRequest pwmRequest ) throws ServletEx
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 );
}
}

Expand All @@ -563,7 +561,8 @@ public ProcessStatus deletePhotoHandler( final PwmRequest pwmRequest ) throws Se
}

@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 );
Expand All @@ -573,10 +572,11 @@ public ProcessStatus readPhotoHandler( final PwmRequest pwmRequest ) throws Serv
{
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();
Expand Down
11 changes: 5 additions & 6 deletions server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
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;
Expand All @@ -64,9 +65,7 @@
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;
Expand Down Expand Up @@ -782,7 +781,7 @@ public static Optional<PhotoDataBean> readPhotoDataFromLdap(
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
{
Expand All @@ -792,8 +791,8 @@ public static Optional<PhotoDataBean> readPhotoDataFromLdap(
{
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 )
{
Expand All @@ -803,7 +802,7 @@ public static Optional<PhotoDataBean> readPhotoDataFromLdap(
{
throw PwmUnrecoverableException.fromChaiException( e );
}
return Optional.of( new PhotoDataBean( mimeType, ImmutableByteArray.of( photoData ) ) );
return Optional.of( new PhotoDataBean( mimeType, photoData ) );
}


Expand Down
2 changes: 2 additions & 0 deletions server/src/main/resources/password/pwm/AppProperty.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion webapp/src/main/webapp/WEB-INF/jsp/fragment/form.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -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});
});
});
Expand Down
Loading

0 comments on commit 19e19c1

Please sign in to comment.