diff --git a/aspect-model-editor-core/src/main/java/org/eclipse/esmf/ame/utils/ModelUtils.java b/aspect-model-editor-core/src/main/java/org/eclipse/esmf/ame/utils/ModelUtils.java index dd45d68b..a7b01609 100644 --- a/aspect-model-editor-core/src/main/java/org/eclipse/esmf/ame/utils/ModelUtils.java +++ b/aspect-model-editor-core/src/main/java/org/eclipse/esmf/ame/utils/ModelUtils.java @@ -107,4 +107,17 @@ private static Try loadFromUri( final URI uri ) { private static Try loadFromUrl( final URL url ) { return Try.ofSupplier( () -> TurtleLoader.openUrl( url ) ).flatMap( TurtleLoader::loadTurtle ); } + + /** + * Sanitizes the file name to remove any path information and retain only the base file name. + * This method is used to ensure that the file name does not contain any directory path components, + * which helps prevent path traversal attacks. It extracts only the file name portion from a given + * string that may represent a path. + * + * @param fileName The file name string potentially including path information. + * @return The sanitized base file name without any path components. + */ + public static String sanitizeFileInformation( String fileName ) { + return new File( fileName ).getName(); + } } diff --git a/aspect-model-editor-repository/src/main/java/org/eclipse/esmf/ame/repository/strategy/utils/LocalFolderResolverUtils.java b/aspect-model-editor-repository/src/main/java/org/eclipse/esmf/ame/repository/strategy/utils/LocalFolderResolverUtils.java index d7fec2e1..d75142a0 100644 --- a/aspect-model-editor-repository/src/main/java/org/eclipse/esmf/ame/repository/strategy/utils/LocalFolderResolverUtils.java +++ b/aspect-model-editor-repository/src/main/java/org/eclipse/esmf/ame/repository/strategy/utils/LocalFolderResolverUtils.java @@ -13,6 +13,8 @@ package org.eclipse.esmf.ame.repository.strategy.utils; +import java.io.File; + import javax.annotation.Nonnull; import org.eclipse.esmf.ame.resolver.strategy.model.FolderStructure; diff --git a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/PackageService.java b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/PackageService.java index 15fccded..db76361a 100644 --- a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/PackageService.java +++ b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/PackageService.java @@ -92,7 +92,7 @@ public FileValidationReport validateAspectModelsForExport( final List validFiles = aspectModelFiles.stream() - .flatMap( data -> data.getFiles().stream().map( fileName -> { + .flatMap( data -> sanitizeIncomingFiles( data.getFiles() ).stream().map( fileName -> { String aspectModel = strategy.getModelAsString( data.getNamespace(), fileName ); AspectModelToExportCache.put( data.getNamespace() + ":" + fileName, aspectModel ); final FileSystemStrategy fileSystemStrategy = new FileSystemStrategy( aspectModel ); @@ -184,23 +184,29 @@ private FileValidationReport validateValidFiles( final String modelStoragePath ) public List importAspectModelPackage( final List aspectModelFiles ) { final ModelResolverStrategy strategy = modelResolverRepository.getStrategy( LocalFolderResolverStrategy.class ); - return aspectModelFiles.stream().flatMap( data -> data.getFiles().stream().map( fileName -> { - try { - final FolderStructure folderStructure = LocalFolderResolverUtils.extractFilePath( data.getNamespace() ); - folderStructure.setFileName( fileName ); - String aspectModel = ResolverUtils.readString( - importFileSystem.getPath( folderStructure.toString() ), StandardCharsets.UTF_8 ); - Optional namespaceVersion = Optional.of( - folderStructure.getFileRootPath() + File.separator + folderStructure.getVersion() ); - - strategy.saveModel( namespaceVersion, Optional.of( fileName ), aspectModel ); - - return folderStructure.toString(); - } catch ( final IOException e ) { - throw new FileNotFoundException( - String.format( "Cannot import Aspect Model with name %s to workspace", fileName ) ); - } - } ) ).toList(); + return aspectModelFiles.stream().flatMap( + data -> sanitizeIncomingFiles( data.getFiles() ).stream().map( fileName -> { + try { + final FolderStructure folderStructure = LocalFolderResolverUtils.extractFilePath( + data.getNamespace() ); + folderStructure.setFileName( fileName ); + String aspectModel = ResolverUtils.readString( + importFileSystem.getPath( folderStructure.toString() ), StandardCharsets.UTF_8 ); + Optional namespaceVersion = Optional.of( + folderStructure.getFileRootPath() + File.separator + folderStructure.getVersion() ); + + strategy.saveModel( namespaceVersion, Optional.of( fileName ), aspectModel ); + + return folderStructure.toString(); + } catch ( final IOException e ) { + throw new FileNotFoundException( + String.format( "Cannot import Aspect Model with name %s to workspace", fileName ) ); + } + } ) ).toList(); + } + + private List sanitizeIncomingFiles( List incomingFiles ) { + return incomingFiles.stream().map( ModelUtils::sanitizeFileInformation ).toList(); } private List getMissingAspectModelFiles( final ViolationReport violationReport, diff --git a/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/FileHandlingController.java b/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/FileHandlingController.java index ba85405d..d291c0fc 100644 --- a/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/FileHandlingController.java +++ b/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/FileHandlingController.java @@ -18,6 +18,7 @@ import org.eclipse.esmf.ame.exceptions.FileNotFoundException; import org.eclipse.esmf.ame.services.FileHandlingService; +import org.eclipse.esmf.ame.utils.ModelUtils; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; @@ -51,7 +52,10 @@ public FileHandlingController( final FileHandlingService fileHandlingService ) { @GetMapping( "lock" ) public ResponseEntity lockFile( @RequestHeader final Map headers ) throws FileNotFoundException { - return processFileOperation( headers, true ); + String sanitizedNamespace = ModelUtils.sanitizeFileInformation( headers.get( NAMESPACE ) ); + String sanitizedFileName = ModelUtils.sanitizeFileInformation( headers.get( FILE_NAME ) ); + + return processFileOperation( sanitizedNamespace, sanitizedFileName, true ); } /** @@ -65,30 +69,38 @@ public ResponseEntity lockFile( @RequestHeader final Map @GetMapping( "unlock" ) public ResponseEntity unlockFile( @RequestHeader final Map headers ) throws FileNotFoundException { - return processFileOperation( headers, false ); + String sanitizedNamespace = ModelUtils.sanitizeFileInformation( headers.get( NAMESPACE ) ); + String sanitizedFileName = ModelUtils.sanitizeFileInformation( headers.get( FILE_NAME ) ); + + return processFileOperation( sanitizedNamespace, sanitizedFileName, false ); } /** - * Processes file operations (lock/unlock) based on the provided namespace and filename. + * Processes file operations (lock/unlock) based on the provided file namespace and filename. + * This method uses the namespace and filename obtained from HTTP headers to determine + * the specific file for the lock/unlock operation. * - * @param headers HTTP headers that contain the namespace and filename. - * @param isLocking Boolean flag to determine the operation type (lock/unlock). - * @return ResponseEntity with operation result. + * @param sanitizedNamespace The namespace associated with the file, extracted from HTTP headers. + * @param sanitizedFileName The name of the file, extracted from HTTP headers. + * @param isLocking Boolean flag to determine the operation type (true for lock, false for unlock). + * @return ResponseEntity with the result of the lock/unlock operation. * - * @throws FileNotFoundException if the filename is not provided. + * @throws FileNotFoundException if the filename is not provided in the headers. + * @throws IllegalArgumentException if either the namespace or filename parameters are invalid. */ - private ResponseEntity processFileOperation( Map headers, boolean isLocking ) + private ResponseEntity processFileOperation( String sanitizedNamespace, String sanitizedFileName, + boolean isLocking ) throws FileNotFoundException { - final String namespace = Optional.ofNullable( headers.get( NAMESPACE ) ).orElse( "" ); - final String filename = Optional.ofNullable( headers.get( FILE_NAME ) ) + final String namespace = Optional.ofNullable( sanitizedNamespace ).orElse( "" ); + final String fileName = Optional.ofNullable( sanitizedFileName ) .orElseThrow( () -> new FileNotFoundException( "Please specify a file name" ) ); - if ( isValidParam( namespace ) && isValidParam( filename ) ) { + if ( isValidParam( namespace ) && isValidParam( fileName ) ) { throw new IllegalArgumentException( "Invalid headers parameter" ); } - return ResponseEntity.ok( isLocking ? fileHandlingService.lockFile( namespace, filename ) - : fileHandlingService.unlockFile( namespace, filename ) ); + return ResponseEntity.ok( isLocking ? fileHandlingService.lockFile( namespace, fileName ) + : fileHandlingService.unlockFile( namespace, fileName ) ); } private boolean isValidParam( String fileName ) {