1515import java .nio .file .StandardCopyOption ;
1616import java .sql .SQLException ;
1717import java .util .ArrayList ;
18+ import java .util .Arrays ;
1819import java .util .Optional ;
20+ import java .util .regex .Pattern ;
1921import java .util .stream .Stream ;
2022import org .slf4j .Logger ;
2123import org .slf4j .LoggerFactory ;
@@ -45,6 +47,65 @@ Path resolveSubPath(final Path rootPath, final Path filePath) {
4547 return resolvedPath ;
4648 }
4749
50+ /**
51+ * Validates that the path does not contain any invalid characters.
52+ *
53+ * Forbidden Characters for File and Folder names:
54+ * < (less than), > (greater than), : (colon), " (double quote), / (forward slash), \ (backslash),
55+ * | (vertical bar or pipe), ? (question mark), * (asterisk),
56+ * % (percent sign - causes issues with URL path resolution as it is not automatically encoded),
57+ * # (pound sign - causes issues with URL path resolution as it is not automatically encoded),
58+ * Unicode Control Characters (0-31, 127-159),
59+ * trailing .
60+ * trailing space
61+ *
62+ * While / (forward slash) is a forbidden characters in filenames, it's interpreted by Java's Path class as a
63+ * folder delineator, meaning that it will not appear as a path segment.
64+ * The character is still checked for just in case.
65+ *
66+ * Reserved Filenames (these are not permitted on Windows even if they have an extension):
67+ * CON, PRN, AUX, NUL, COM1, COM2, COM3, COM4, COM5, COM6, COM7, COM8, COM9,
68+ * LPT1, LPT2, LPT3, LPT4, LPT5, LPT6, LPT7, LPT8, LPT9
69+ *
70+ * @param path the Path to validate
71+ */
72+ void validatePath (final Path path ) throws WorkspaceFileOpException {
73+ final String [] reservedFilenames = {"CON" , "PRN" , "AUX" , "NUL" , "COM1" , "COM2" , "COM3" , "COM4" , "COM5" , "COM6" , "COM7" ,
74+ "COM8" , "COM9" , "LPT1" , "LPT2" , "LPT3" , "LPT4" , "LPT5" , "LPT6" , "LPT7" , "LPT8" , "LPT9" };
75+ final var controlCharacters = Pattern .compile ("([\u0000 -\u001F ]|[\u007F -\u009F ])+" , Pattern .UNICODE_CHARACTER_CLASS );
76+ final var forbiddenCharacters = Pattern .compile ("([|<>:/\" ?*%#\\ \\ ])+" , Pattern .UNICODE_CHARACTER_CLASS );
77+
78+
79+ for (final var pathSegment : path ) {
80+ final var segment = pathSegment .toString ();
81+ // Check for trailing period or space
82+ if (segment .endsWith (" " )) {
83+ throw new WorkspaceFileOpException ("Path segment '" + segment + "' cannot end in a space." );
84+ }
85+ if (segment .endsWith ("." )) {
86+ throw new WorkspaceFileOpException ("Path segment '" + segment + "' cannot end in a period." );
87+ }
88+
89+ // Check for control characters
90+ final var controlMatcher = controlCharacters .matcher (segment );
91+ if (controlMatcher .find ()){
92+ throw new WorkspaceFileOpException ("Path segment '" + segment + "' has illegal characters: " +controlMatcher .group ());
93+ }
94+
95+ // Check for forbidden characters
96+ final var forbiddenMatcher = forbiddenCharacters .matcher (segment );
97+ if (forbiddenMatcher .find ()){
98+ throw new WorkspaceFileOpException ("Path segment '" + segment + "' has illegal characters: " +forbiddenMatcher .group ());
99+ }
100+
101+ // Check that the segment is not a reserved filenames:
102+ final var name = segment .split ("\\ ." )[0 ];
103+ if (Arrays .asList (reservedFilenames ).contains (name )){
104+ throw new WorkspaceFileOpException ("Path segment '" + segment + "' contains reserved name: " +name );
105+ }
106+ }
107+ }
108+
48109 public WorkspaceFileSystemService (final WorkspacePostgresRepository postgresRepository ) {
49110 this .postgresRepository = postgresRepository ;
50111 }
@@ -149,26 +210,29 @@ public FileStream loadFile(final int workspaceId, final Path filePath) throws IO
149210
150211 @ Override
151212 public boolean saveFile (final int workspaceId , final Path filePath , final UploadedFile file )
152- throws NoSuchWorkspaceException {
213+ throws NoSuchWorkspaceException , WorkspaceFileOpException {
153214 final var repoPath = postgresRepository .workspaceRootPath (workspaceId );
154215 final var path = resolveSubPath (repoPath , filePath );
155216
156217 if (path .toFile ().isDirectory ()) return false ;
157218
219+ validatePath (path );
158220 FileUtil .streamToFile (file .content (), path .toString ());
159221 return true ;
160222 }
161223
162224 @ Override
163225 public boolean moveFile (final int oldWorkspaceId , final Path oldFilePath , final int newWorkspaceId , final Path newFilePath )
164- throws NoSuchWorkspaceException , SQLException
226+ throws NoSuchWorkspaceException , SQLException , WorkspaceFileOpException
165227 {
166228 final var oldRepoPath = postgresRepository .workspaceRootPath (oldWorkspaceId );
167229 final var oldPath = resolveSubPath (oldRepoPath , oldFilePath );
168230 final var newRepoPath = (oldWorkspaceId == newWorkspaceId ) ? oldRepoPath : postgresRepository .workspaceRootPath (newWorkspaceId );
169231 final var newPath = resolveSubPath (newRepoPath , newFilePath );
170232 boolean success = true ;
171233
234+ validatePath (newPath );
235+
172236 // Find hidden metadata files, if they exist, and move them
173237 final var metadataExtensions = postgresRepository .getMetadataExtensions ();
174238 for (final var extension : metadataExtensions ) {
@@ -191,6 +255,8 @@ public boolean copyFile(final int sourceWorkspaceId, final Path sourceFilePath,
191255 final var destRepoPath = (sourceWorkspaceId == destWorkspaceId ) ? sourceRepoPath : postgresRepository .workspaceRootPath (destWorkspaceId );
192256 final var destPath = resolveSubPath (destRepoPath , destFilePath );
193257
258+ validatePath (destPath );
259+
194260 try {
195261 // Do not copy the file if the source file does not exist
196262 if (!sourcePath .toFile ().exists ()) throw new WorkspaceFileOpException ("Source file \" %s\" in workspace %d does not exist." .formatted (sourceFilePath , sourceWorkspaceId ));
@@ -228,10 +294,11 @@ public boolean copyDirectory(final int sourceWorkspaceId, final Path sourceFileP
228294 final var destRepoPath = (sourceWorkspaceId == destWorkspaceId ) ? sourceRepoPath : postgresRepository .workspaceRootPath (destWorkspaceId );
229295 final var destPath = resolveSubPath (destRepoPath , destFilePath );
230296
297+ validatePath (destPath );
231298 try {
232299 // Validate source exists and is a directory
233300 if (!Files .exists (sourcePath )) throw new WorkspaceFileOpException ("Source directory \" %s\" in workspace %d does not exist." .formatted (sourceFilePath , sourceWorkspaceId ));
234- if (!Files .isDirectory (sourcePath )) throw new WorkspaceFileOpException ("Source directory \" %s\" in workspace %d is not actually a directory." .formatted (sourceFilePath , sourceWorkspaceId ));
301+ if (!Files .isDirectory (sourcePath )) throw new WorkspaceFileOpException ("Source directory \" %s\" in workspace %d is not a directory." .formatted (sourceFilePath , sourceWorkspaceId ));
235302
236303 // Do not try to copy a directory into itself
237304 if (sourceWorkspaceId == destWorkspaceId && destPath .startsWith (sourcePath )){
@@ -304,9 +371,11 @@ public DirectoryTree listFiles(final int workspaceId, final Optional<Path> direc
304371 }
305372
306373 @ Override
307- public boolean createDirectory (final int workspaceId , final Path directoryPath ) throws IOException , NoSuchWorkspaceException {
374+ public boolean createDirectory (final int workspaceId , final Path directoryPath )
375+ throws IOException , NoSuchWorkspaceException , WorkspaceFileOpException {
308376 final var repoPath = postgresRepository .workspaceRootPath (workspaceId );
309377 final var path = resolveSubPath (repoPath , directoryPath );
378+ validatePath (path );
310379 Files .createDirectories (path );
311380 return true ;
312381 }
@@ -320,6 +389,7 @@ public boolean moveDirectory(final int oldWorkspaceId, final Path oldDirectoryPa
320389 final var oldPath = resolveSubPath (oldRepoPath , oldDirectoryPath );
321390 final var newRepoPath = (oldWorkspaceId == newWorkspaceId ) ? oldRepoPath : postgresRepository .workspaceRootPath (newWorkspaceId ).normalize ();
322391 final var newPath = resolveSubPath (newRepoPath , newDirectoryPath );
392+ validatePath (newPath );
323393
324394 // Do not permit the source workspace's root directory to be moved
325395 if (Files .isSameFile (oldPath , oldRepoPath )) throw new WorkspaceFileOpException ("Cannot move the workspace root directory." );
0 commit comments