Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PPP-5021] Use a secure SAX parser which forbids external entities and #5541

Merged
merged 1 commit into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 180 additions & 10 deletions extensions/src/main/java/org/pentaho/platform/web/http/api/resources/FileResource.java
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* See the GNU Lesser General Public License for more details.
*
*
* Copyright (c) 2002-2020 Hitachi Vantara. All rights reserved.
* Copyright (c) 2002-2024 Hitachi Vantara. All rights reserved.
*
*/

Expand Down Expand Up @@ -87,9 +87,20 @@
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Providers;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stream.StreamSource;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
Expand Down Expand Up @@ -139,6 +150,8 @@ public class FileResource extends AbstractJaxRSResource {

protected static UserRoleListService userRoleListService;

private @Context Providers providers;

IRepositoryContentConverterHandler converterHandler;
Map<String, Converter> converters;

Expand Down Expand Up @@ -725,7 +738,7 @@ public Response doGetFileAsInline( @PathParam ( "pathId" ) String pathId ) {
* </p>
*
* @param pathId Colon separated path for the repository file.
* @param acl Acl of the repository file RepositoryFileAclDto.
* @param aclXml Acl of the repository file RepositoryFileAclDto.
*
* @return A jax-rs Response object with the appropriate status code, header, and body.
*
Expand All @@ -736,13 +749,70 @@ public Response doGetFileAsInline( @PathParam ( "pathId" ) String pathId ) {
*/
@PUT
@Path ( "{pathId : .+}/acl" )
@Consumes ( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON } )
@Consumes ( { MediaType.APPLICATION_XML } )
@StatusCodes ( {
@ResponseCode ( code = 200, condition = "Successfully saved file." ),
@ResponseCode ( code = 403, condition = "Failed to save acls due to missing or incorrect properties." ),
@ResponseCode ( code = 400, condition = "Failed to save acls due to malformed xml." ),
@ResponseCode ( code = 500, condition = "Failed to save acls due to another error." ) } )
public Response setFileAcls( @PathParam ( "pathId" ) String pathId, RepositoryFileAclDto acl ) {
public Response setFileAcls( @PathParam ( "pathId" ) String pathId, StreamSource aclXml ) {
/*
* [BISERVER-14294] Ensuring the owner is set to a non-null, non-empty string value to prevent any issues
* that might cause problems with the repository. Then following it up with a user existence check
*
* [BISERVER-14409] What we need to check is that the users or roles don't contain invalid characters.
*
* We can't check if a user or a role exist, some scenarios like mixed AuthZ / AuthN are unable of confirming
* that a user or role exist. One of such scenarios is with SAML.
*/

try {
Unmarshaller unmarshaller = getUnmarshaller( RepositoryFileAclDto.class );
XMLStreamReader xsr = getSecureXmlStreamReader( aclXml );
RepositoryFileAclDto acl = (RepositoryFileAclDto) unmarshaller.unmarshal( xsr );
if ( validateUsersAndRoles( acl ) ) {
fileService.setFileAcls( pathId, acl );
return buildOkResponse();
} else {
logger.error( getMessagesInstance().getString( "SystemResource.GENERAL_ERROR" ) );
return buildStatusResponse( Response.Status.FORBIDDEN );
}
} catch ( Exception exception ) {
logger.error( getMessagesInstance().getString( "SystemResource.GENERAL_ERROR" ), exception );
return buildStatusResponse( Response.Status.INTERNAL_SERVER_ERROR );
}
}

/**
* This method is used to update and save the acls of the selected file to the repository.
*
* <p><b>Example Request:</b><br />
* PUT pentaho/api/repo/files/:jmeter-test:test_file_1.xml/acl
* <br /><b>PUT data:</b>
* <pre function="syntax.xml">
* &lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;yes&quot;?&gt;&lt;repositoryFileAclDto&gt;&lt;entriesInheriting&gt;true&lt;/entriesInheriting&gt;&lt;id&gt;d45d4972-989e-48d5-8bd0-f7024a77f08f&lt;/id&gt;&lt;owner&gt;admin&lt;/owner&gt;&lt;ownerType&gt;0&lt;/ownerType&gt;&lt;/repositoryFileAclDto&gt;
* </pre>
* </p>
*
* @param pathId Colon separated path for the repository file.
* @param acl Acl of the repository file RepositoryFileAclDto.
*
* @return A jax-rs Response object with the appropriate status code, header, and body.
*
* <p><b>Example Response:</b></p>
* <pre function="syntax.xml">
* This response does not contain data.
* </pre>
*/
@PUT
@Path ( "{pathId : .+}/acl" )
@Consumes ( { MediaType.APPLICATION_JSON } )
@StatusCodes ( {
@ResponseCode ( code = 200, condition = "Successfully saved file." ),
@ResponseCode ( code = 403, condition = "Failed to save acls due to missing or incorrect properties." ),
@ResponseCode ( code = 400, condition = "Failed to save acls due to malformed xml." ),
@ResponseCode ( code = 500, condition = "Failed to save acls due to another error." ) } )
public Response setFileAcls( @PathParam ( "pathId" ) String pathId, RepositoryFileAclDto acl ) {
/*
* [BISERVER-14294] Ensuring the owner is set to a non-null, non-empty string value to prevent any issues
* that might cause problems with the repository. Then following it up with a user existence check
Expand All @@ -766,14 +836,38 @@ public Response setFileAcls( @PathParam ( "pathId" ) String pathId, RepositoryFi
}
}

protected XMLStreamReader getSecureXmlStreamReader( StreamSource xmlSource ) throws XMLStreamException {
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.setProperty( XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false );
xif.setProperty( XMLInputFactory.SUPPORT_DTD, false );
XMLStreamReader xsr = xif.createXMLStreamReader( xmlSource );
return xsr;
}

protected Unmarshaller getUnmarshaller( Class<?> clazz ) throws JAXBException {
ContextResolver<JAXBContext> jaxbResolver = null;
JAXBContext jaxbContext = null;
if ( null != providers ) { // should never be null except in unit tests
jaxbResolver = providers.getContextResolver( JAXBContext.class, MediaType.APPLICATION_XML_TYPE );
}
if ( null != jaxbResolver ) {
jaxbContext = jaxbResolver.getContext( clazz );
}
if ( null == jaxbContext ) {
jaxbContext = JAXBContext.newInstance( clazz );
}
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
return unmarshaller;
}

/**
* Store content creator for the given path of created content.
*
* @param pathId colon separated path for the repository file that was created by the contenCreator below
* <pre function="syntax.xml">
* :path:to:file:id
* </pre>
* @param contentCreator Repository file that created the file at the above pathId location
* @param contentCreatorXml Repository file that created the file at the above pathId location
* <pre function="syntax.xml">
* &lt;repositoryFileDto&gt;
* &lt;createdDate&gt;1402911997019&lt;/createdDate&gt;
Expand Down Expand Up @@ -820,11 +914,86 @@ public Response setFileAcls( @PathParam ( "pathId" ) String pathId, RepositoryFi
*/
@PUT
@Path ( "{pathId : .+}/creator" )
@Consumes ( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON } )
@Consumes ( { MediaType.APPLICATION_XML } )
@StatusCodes ( {
@ResponseCode ( code = 200, condition = "Successfully retrieved file." ),
@ResponseCode ( code = 500, condition = "Failed to download file because of some other error." ) } )
@Facet ( name = "Unsupported" )
public Response doSetContentCreator( @PathParam ( "pathId" ) String pathId, StreamSource contentCreatorXml ) {
try {
Unmarshaller unmarshaller = getUnmarshaller( RepositoryFileDto.class );
XMLStreamReader xsr = getSecureXmlStreamReader( contentCreatorXml );
RepositoryFileDto contentCreator = (RepositoryFileDto) unmarshaller.unmarshal( xsr );
fileService.doSetContentCreator( pathId, contentCreator );
return buildOkResponse();
} catch ( FileNotFoundException e ) {
logger.error( getMessagesInstance().getErrorString( "FileResource.FILE_NOT_FOUND", pathId ), e );
return buildStatusResponse( Response.Status.NOT_FOUND );
} catch ( Throwable t ) {
logger.error( getMessagesInstance().getString( "SystemResource.GENERAL_ERROR" ), t );
return buildStatusResponse( Response.Status.INTERNAL_SERVER_ERROR );
}
}

/**
* Store content creator for the given path of created content.
*
* @param pathId colon separated path for the repository file that was created by the contenCreator below
* <pre function="syntax.xml">
* :path:to:file:id
* </pre>
* @param contentCreator Repository file that created the file at the above pathId location
* <pre function="syntax.xml">
* &lt;repositoryFileDto&gt;
* &lt;createdDate&gt;1402911997019&lt;/createdDate&gt;
* &lt;fileSize&gt;3461&lt;/fileSize&gt;
* &lt;folder&gt;false&lt;/folder&gt;
* &lt;hidden&gt;false&lt;/hidden&gt;
* &lt;id&gt;ff11ac89-7eda-4c03-aab1-e27f9048fd38&lt;/id&gt;
* &lt;lastModifiedDate&gt;1406647160536&lt;/lastModifiedDate&gt;
* &lt;locale&gt;en&lt;/locale&gt;
* &lt;localePropertiesMapEntries&gt;
* &lt;localeMapDto&gt;
* &lt;locale&gt;default&lt;/locale&gt;
* &lt;properties&gt;
* &lt;stringKeyStringValueDto&gt;
* &lt;key&gt;file.title&lt;/key&gt;
* &lt;value&gt;myFile&lt;/value&gt;
* &lt;/stringKeyStringValueDto&gt;
* &lt;stringKeyStringValueDto&gt;
* &lt;key&gt;jcr:primaryType&lt;/key&gt;
* &lt;value&gt;nt:unstructured&lt;/value&gt;
* &lt;/stringKeyStringValueDto&gt;
* &lt;stringKeyStringValueDto&gt;
* &lt;key&gt;title&lt;/key&gt;
* &lt;value&gt;myFile&lt;/value&gt;
* &lt;/stringKeyStringValueDto&gt;
* &lt;stringKeyStringValueDto&gt;
* &lt;key&gt;file.description&lt;/key&gt;
* &lt;value&gt;myFile Description&lt;/value&gt;
* &lt;/stringKeyStringValueDto&gt;
* &lt;/properties&gt;
* &lt;/localeMapDto&gt;
* &lt;/localePropertiesMapEntries&gt;
* &lt;locked&gt;false&lt;/locked&gt;
* &lt;name&gt;myFile.prpt&lt;/name&gt;&lt;/name&gt;
* &lt;originalParentFolderPath&gt;/public/admin&lt;/originalParentFolderPath&gt;
* &lt;ownerType&gt;-1&lt;/ownerType&gt;
* &lt;path&gt;/public/admin/ff11ac89-7eda-4c03-aab1-e27f9048fd38&lt;/path&gt;
* &lt;title&gt;myFile&lt;/title&gt;
* &lt;versionId&gt;1.9&lt;/versionId&gt;
* &lt;versioned&gt;true&lt;/versioned&gt;
* &lt;/repositoryFileAclDto&gt;
* </pre>
* @return A jax-rs Response object with the appropriate status code, header, and body.
*/
@PUT
@Path ( "{pathId : .+}/creator" )
@Consumes ( { MediaType.APPLICATION_JSON } )
@StatusCodes ( {
@ResponseCode ( code = 200, condition = "Successfully retrieved file." ),
@ResponseCode ( code = 500, condition = "Failed to download file because of some other error." ) } )
@Facet ( name = "Unsupported" )
public Response doSetContentCreator( @PathParam ( "pathId" ) String pathId, RepositoryFileDto contentCreator ) {
try {
fileService.doSetContentCreator( pathId, contentCreator );
Expand Down Expand Up @@ -1994,11 +2163,12 @@ public Response doRename( @PathParam( "pathId" ) String pathId, @QueryParam( "ne
@PUT
@Path ( "{pathId : .+}/metadata" )
@Produces ( { MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON } )
@Consumes ( { MediaType.APPLICATION_JSON } )
@StatusCodes ( {
@ResponseCode ( code = 200, condition = "Successfully retrieved metadata." ),
@ResponseCode ( code = 403, condition = "Invalid path." ),
@ResponseCode ( code = 400, condition = "Invalid payload." ),
@ResponseCode ( code = 500, condition = "Server Error." ) } )
@ResponseCode ( code = 200, condition = "Successfully retrieved metadata." ),
@ResponseCode ( code = 403, condition = "Invalid path." ),
@ResponseCode ( code = 400, condition = "Invalid payload." ),
@ResponseCode ( code = 500, condition = "Server Error." ) } )
public Response doSetMetadata( @PathParam ( "pathId" ) String pathId, List<StringKeyStringValueDto> metadata ) {
try {
fileService.doSetMetadata( pathId, metadata );
Expand Down
Loading
Loading