Skip to content

Commit 6d192dd

Browse files
committed
improve photo upload/download mime type enforcement
1 parent 825d2d9 commit 6d192dd

File tree

12 files changed

+82
-74
lines changed

12 files changed

+82
-74
lines changed

server/src/main/java/password/pwm/AppProperty.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ public enum AppProperty
325325
SECURITY_HTTP_PERFORM_CSRF_HEADER_CHECKS ( "security.http.performCsrfHeaderChecks" ),
326326
SECURITY_HTTP_PROMISCUOUS_ENABLE ( "security.http.promiscuousEnable" ),
327327
SECURITY_HTTP_CONFIG_CSP_HEADER ( "security.http.config.cspHeader" ),
328+
SECURITY_HTTP_USER_PHOTO_MIME_TYPES ( "security.http.permittedUserPhotoMimeTypes" ),
329+
SECURITY_HTTP_PERMITTED_URL_PATH_CHARS ( "security.http.permittedUrlPathCharacters" ),
328330
SECURITY_HTTPSSERVER_SELF_FUTURESECONDS ( "security.httpsServer.selfCert.futureSeconds" ),
329331
SECURITY_HTTPSSERVER_SELF_ALG ( "security.httpsServer.selfCert.alg" ),
330332
SECURITY_HTTPSSERVER_SELF_KEY_SIZE ( "security.httpsServer.selfCert.keySize" ),

server/src/main/java/password/pwm/config/Configuration.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,11 @@ public LdapProfile getDefaultLdapProfile( ) throws PwmUnrecoverableException
403403
return getLdapProfiles().values().iterator().next();
404404
}
405405

406+
public List<String> permittedPhotoMimeTypes()
407+
{
408+
final String permittedMimeTypesStr = readAppProperty( AppProperty.SECURITY_HTTP_USER_PHOTO_MIME_TYPES );
409+
return List.copyOf( StringUtil.splitAndTrim( permittedMimeTypesStr, "," ) );
410+
}
406411

407412
public List<Locale> getKnownLocales( )
408413
{

server/src/main/java/password/pwm/config/value/FormValue.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ public String toDebugString( final Locale locale )
191191
}
192192
if ( formRow.getType() == FormConfiguration.Type.photo )
193193
{
194-
sb.append( " MimeTypes: " ).append( StringUtil.collectionToString( formRow.getMimeTypes() ) ).append( "\n" );
195194
sb.append( " MaxSize: " ).append( formRow.getMaximumSize() ).append( "\n" );
196195
}
197196

server/src/main/java/password/pwm/config/value/data/FormConfiguration.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import java.io.Serializable;
3939
import java.math.BigInteger;
4040
import java.util.ArrayList;
41-
import java.util.Arrays;
4241
import java.util.Collection;
4342
import java.util.Collections;
4443
import java.util.List;
@@ -123,15 +122,6 @@ public enum Source
123122
@Builder.Default
124123
private Map<String, String> selectOptions = Collections.emptyMap();
125124

126-
@Builder.Default
127-
private List<String> mimeTypes = Arrays.asList(
128-
"image/gif",
129-
"image/png",
130-
"image/jpeg",
131-
"image/bmp",
132-
"image/webp"
133-
);
134-
135125
@Builder.Default
136126
private int maximumSize = 65000;
137127

server/src/main/java/password/pwm/http/PwmURL.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,12 @@ public String getPostServletPath( final PwmServletDefinition pwmServletDefinitio
423423
return "";
424424
}
425425

426+
public List<String> getPathSegments()
427+
{
428+
final String uriPath = uri.getPath();
429+
return StringUtil.splitAndTrim( uriPath, "/" );
430+
}
431+
426432
public String determinePwmServletPath( )
427433
{
428434
final String requestPath = this.uri.getPath();

server/src/main/java/password/pwm/http/filter/RequestInitializationFilter.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
import java.util.List;
7474
import java.util.Locale;
7575
import java.util.Map;
76+
import java.util.regex.Pattern;
7677

7778
public class RequestInitializationFilter implements Filter
7879
{
@@ -585,6 +586,9 @@ private static void handleRequestSecurityChecks( final PwmRequest pwmRequest )
585586
// check the user's IP address
586587
checkIfSourceAddressChanged( pwmRequest );
587588

589+
// check url path segments
590+
checkURlPathSegments( pwmRequest );
591+
588592
// check total time.
589593
checkTotalSessionTime( pwmRequest );
590594

@@ -718,6 +722,31 @@ private static void checkSourceNetworkAddress( final PwmRequest pwmRequest )
718722
}
719723
}
720724

725+
private static void checkURlPathSegments( final PwmRequest pwmRequest )
726+
throws PwmUnrecoverableException
727+
{
728+
if ( pwmRequest.getURL().isResourceURL() )
729+
{
730+
return;
731+
}
732+
733+
final String checkRegexPatternString = pwmRequest.getConfig().readAppProperty( AppProperty.SECURITY_HTTP_PERMITTED_URL_PATH_CHARS );
734+
if ( StringUtil.isEmpty( checkRegexPatternString ) )
735+
{
736+
return;
737+
}
738+
739+
final Pattern pattern = Pattern.compile( checkRegexPatternString );
740+
for ( final String pathPart : pwmRequest.getURL().getPathSegments() )
741+
{
742+
if ( !pattern.matcher( pathPart ).matches() )
743+
{
744+
final String errorMsg = "request URL path segment contains illegal characters";
745+
final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SECURITY_VIOLATION, errorMsg );
746+
throw new PwmUnrecoverableException( errorInformation );
747+
}
748+
}
749+
}
721750

722751
private static void checkCsrfHeader( final PwmRequest pwmRequest )
723752
throws PwmUnrecoverableException

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

Lines changed: 12 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import password.pwm.http.PwmRequest;
4444
import password.pwm.http.PwmRequestAttribute;
4545
import password.pwm.http.PwmSession;
46+
import password.pwm.http.bean.ImmutableByteArray;
4647
import password.pwm.http.bean.UpdateProfileBean;
4748
import password.pwm.http.servlet.ControlledPwmServlet;
4849
import password.pwm.i18n.Message;
@@ -52,6 +53,7 @@
5253
import password.pwm.svc.token.TokenService;
5354
import password.pwm.svc.token.TokenType;
5455
import password.pwm.svc.token.TokenUtil;
56+
import password.pwm.util.ServletUtility;
5557
import password.pwm.util.form.FormUtility;
5658
import password.pwm.util.java.JavaHelper;
5759
import password.pwm.util.java.JsonUtil;
@@ -64,13 +66,11 @@
6466
import javax.servlet.annotation.WebServlet;
6567
import javax.servlet.http.HttpServletRequest;
6668
import javax.servlet.http.HttpServletResponse;
67-
import java.io.ByteArrayInputStream;
6869
import java.io.ByteArrayOutputStream;
6970
import java.io.IOException;
7071
import java.io.InputStream;
7172
import java.io.OutputStream;
7273
import java.io.Serializable;
73-
import java.net.URLConnection;
7474
import java.util.Collection;
7575
import java.util.Collections;
7676
import java.util.List;
@@ -522,22 +522,14 @@ public ProcessStatus uploadPhoto( final PwmRequest pwmRequest ) throws ServletEx
522522
return ProcessStatus.Halt;
523523
}
524524

525-
final String b64String = StringUtil.base64Encode( bytes );
526-
527-
if ( !JavaHelper.isEmpty( formConfiguration.getMimeTypes() ) )
525+
if ( ServletUtility.mimeTypeForUserPhoto( pwmRequest.getConfig(), ImmutableByteArray.of( bytes ) ).isEmpty() )
528526
{
529-
final String mimeType = URLConnection.guessContentTypeFromStream( new ByteArrayInputStream( bytes ) );
530-
if ( !formConfiguration.getMimeTypes().contains( mimeType ) )
531-
{
532-
final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_FILE_TYPE_INCORRECT, "incorrect file type of " + mimeType, new String[]
533-
{
534-
mimeType,
535-
}
536-
);
537-
pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
538-
return ProcessStatus.Halt;
539-
}
527+
final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_FILE_TYPE_INCORRECT, "unsupported mime type" );
528+
pwmRequest.outputJsonResult( RestResultBean.fromError( errorInformation, pwmRequest ) );
529+
return ProcessStatus.Halt;
540530
}
531+
532+
final String b64String = StringUtil.base64Encode( bytes );
541533
updateProfileBean.getFormData().put( fieldName, b64String );
542534
}
543535
}
@@ -560,7 +552,8 @@ public ProcessStatus deletePhotoHandler( final PwmRequest pwmRequest ) throws Se
560552
}
561553

562554
@ActionHandler( action = "readPhoto" )
563-
public ProcessStatus readPhotoHandler( final PwmRequest pwmRequest ) throws ServletException, PwmUnrecoverableException, IOException
555+
public ProcessStatus readPhotoHandler( final PwmRequest pwmRequest )
556+
throws ServletException, PwmUnrecoverableException, IOException
564557
{
565558
final String fieldName = pwmRequest.readParameterAsString( "field" );
566559
final UpdateProfileBean updateProfileBean = getBean( pwmRequest );
@@ -570,10 +563,11 @@ public ProcessStatus readPhotoHandler( final PwmRequest pwmRequest ) throws Serv
570563
{
571564
final byte[] bytes = StringUtil.base64Decode( b64value );
572565

566+
final String mimeType = ServletUtility.mimeTypeForUserPhoto( pwmRequest.getConfig(), ImmutableByteArray.of( bytes ) );
567+
573568
try ( OutputStream outputStream = pwmRequest.getPwmResponse().getOutputStream() )
574569
{
575570
final HttpServletResponse resp = pwmRequest.getPwmResponse().getHttpServletResponse();
576-
final String mimeType = URLConnection.guessContentTypeFromStream( new ByteArrayInputStream( bytes ) );
577571
resp.setContentType( mimeType );
578572
outputStream.write( bytes );
579573
outputStream.flush();

server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
import password.pwm.svc.stats.Statistic;
5858
import password.pwm.svc.stats.StatisticsManager;
5959
import password.pwm.util.PasswordData;
60+
import password.pwm.util.ServletUtility;
6061
import password.pwm.util.i18n.LocaleHelper;
6162
import password.pwm.util.java.StringUtil;
6263
import password.pwm.util.java.TimeDuration;
@@ -65,9 +66,7 @@
6566
import password.pwm.util.secure.PwmTrustManager;
6667

6768
import javax.net.ssl.X509TrustManager;
68-
import java.io.ByteArrayInputStream;
6969
import java.io.IOException;
70-
import java.net.URLConnection;
7170
import java.security.cert.X509Certificate;
7271
import java.time.Instant;
7372
import java.util.Arrays;
@@ -940,7 +939,7 @@ public static Optional<PhotoDataBean> readPhotoDataFromLdap(
940939
throw new PwmOperationalException( new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE, "user has no photo data stored in LDAP attribute" ) );
941940
}
942941
photoData = photoAttributeData[ 0 ];
943-
mimeType = URLConnection.guessContentTypeFromStream( new ByteArrayInputStream( photoData ) );
942+
mimeType = ServletUtility.mimeTypeForUserPhoto( configuration, ImmutableByteArray.of( photoData ) );
944943
}
945944
catch ( final IOException | ChaiOperationException e )
946945
{

server/src/main/java/password/pwm/util/ServletUtility.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,20 @@
2222

2323
import org.apache.commons.io.IOUtils;
2424
import password.pwm.PwmConstants;
25+
import password.pwm.config.Configuration;
2526
import password.pwm.error.ErrorInformation;
2627
import password.pwm.error.PwmError;
2728
import password.pwm.error.PwmUnrecoverableException;
29+
import password.pwm.http.bean.ImmutableByteArray;
30+
import password.pwm.util.java.StringUtil;
2831

2932
import javax.servlet.http.HttpServletRequest;
3033
import java.io.IOException;
3134
import java.io.InputStreamReader;
3235
import java.io.Reader;
3336
import java.io.StringWriter;
37+
import java.net.URLConnection;
38+
import java.util.List;
3439

3540
public final class ServletUtility
3641
{
@@ -69,4 +74,21 @@ public static String readRequestBodyAsString( final HttpServletRequest req, fina
6974
}
7075
return stringValue;
7176
}
77+
78+
public static String mimeTypeForUserPhoto(
79+
final Configuration configuration,
80+
final ImmutableByteArray immutableByteArray
81+
)
82+
throws IOException, PwmUnrecoverableException
83+
{
84+
final List<String> permittedMimeTypes = configuration.permittedPhotoMimeTypes();
85+
86+
final String mimeType = URLConnection.guessContentTypeFromStream( immutableByteArray.newByteArrayInputStream() );
87+
if ( !StringUtil.isEmpty( mimeType ) && permittedMimeTypes.contains( mimeType ) )
88+
{
89+
return mimeType;
90+
}
91+
final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_FILE_TYPE_INCORRECT, "unsupported mime type" );
92+
throw new PwmUnrecoverableException( errorInformation );
93+
}
7294
}

server/src/main/resources/password/pwm/AppProperty.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,8 @@ security.http.forceRequestSequencing=false
304304
security.http.stripHeaderRegex=\\n|\\r|(?ism)%0A|%0D
305305
security.http.performCsrfHeaderChecks=false
306306
security.http.promiscuousEnable=false
307+
security.http.permittedUserPhotoMimeTypes=image/gif,image/png,image/jpeg
308+
security.http.permittedUrlPathCharacters=^[a-zA-Z0-9-]*$
307309
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
308310
security.httpsServer.selfCert.futureSeconds=63113904
309311
security.httpsServer.selfCert.alg=RSA

0 commit comments

Comments
 (0)