From 1123c78ed0dd5a00fe68ca954689ef1e79310904 Mon Sep 17 00:00:00 2001 From: Kyle Berezin Date: Thu, 24 Mar 2022 22:05:07 -0400 Subject: [PATCH] create parent dir on fileio:write operation (#920) * creates parent dir on fileio:write * adds properties: security.file.enabled, security.file.strict Co-authored-by: Tres Finocchiaro --- src/qz/common/Constants.java | 3 ++ src/qz/common/TrayManager.java | 8 +++-- src/qz/utils/FileUtilities.java | 61 +++++++++++++++++++++++++++----- src/qz/ws/PrintSocketClient.java | 2 +- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/src/qz/common/Constants.java b/src/qz/common/Constants.java index ec600a828..413c7b0d9 100644 --- a/src/qz/common/Constants.java +++ b/src/qz/common/Constants.java @@ -64,6 +64,9 @@ public class Constants { public static final String PREFS_IDLE_PRINTERS = "tray.idle.printers"; public static final String PREFS_IDLE_JFX = "tray.idle.javafx"; + public static final String PREFS_FILEIO_ENABLED = "security.file.enabled"; + public static final String PREFS_FILEIO_STRICT = "security.file.strict"; + public static final String ALLOW_SITES_TEXT = "Permanently allowed \"%s\" to access local resources"; public static final String BLOCK_SITES_TEXT = "Permanently blocked \"%s\" from accessing local resources"; diff --git a/src/qz/common/TrayManager.java b/src/qz/common/TrayManager.java index 76651a309..4a0cebd6a 100644 --- a/src/qz/common/TrayManager.java +++ b/src/qz/common/TrayManager.java @@ -95,13 +95,17 @@ public TrayManager(boolean isHeadless) { // Set strict certificate mode preference Certificate.setTrustBuiltIn(!getPref(Constants.PREFS_STRICT_MODE, false)); - //headless if turned on by user or unsupported by environment + // Set FileIO security + FileUtilities.setFileIoEnabled(getPref(Constants.PREFS_FILEIO_ENABLED, true)); + FileUtilities.setFileIoStrict(getPref(Constants.PREFS_FILEIO_STRICT, false)); + + // Headless if turned on by user or unsupported by environment headless = isHeadless || getPref(Constants.PREFS_HEADLESS, false) || GraphicsEnvironment.isHeadless(); if (headless) { log.info("Running in headless mode"); } - // Setup the shortcut name so that the UI components can use it + // Set up the shortcut name so that the UI components can use it shortcutCreator = ShortcutCreator.getInstance(); SystemUtilities.setSystemLookAndFeel(); diff --git a/src/qz/utils/FileUtilities.java b/src/qz/utils/FileUtilities.java index f25cb33c4..5284f45e9 100644 --- a/src/qz/utils/FileUtilities.java +++ b/src/qz/utils/FileUtilities.java @@ -217,21 +217,64 @@ public static Path inheritParentPermissions(Path filePath) { private static HashMap localFileMap = new HashMap<>(); private static HashMap sharedFileMap = new HashMap<>(); private static ArrayList> whiteList; + private static boolean FILE_IO_ENABLED = true; + private static boolean FILE_IO_STRICT = false; + + public static void setFileIoEnabled(boolean enabled) { + FILE_IO_ENABLED = enabled; + } + + public static void setFileIoStrict(boolean strict) { + FILE_IO_STRICT = strict; + } + + /** + * Performs security checks before allowing File IO operations: + * 1. Is the request verified (was the signature OK?)? + * 2. Is the certificate valid? + * 3. Is the location whitelisted? + * 4. Is the file extension permitted + */ + private static void checkFileRequest(Path path, FileParams fp, RequestState request, boolean allowRootDir) throws AccessDeniedException { + if(!FILE_IO_ENABLED) { + throw new AccessDeniedException("File operations are disabled"); + } else if(!request.isVerified() && FILE_IO_STRICT) { + throw new AccessDeniedException("File requests is not verified"); + } else if(request.getCertUsed() == null || !request.getCertUsed().isTrusted()) { + throw new AccessDeniedException("Certificate provided is not trusted"); + } else if(!isWhiteListed(path, allowRootDir, fp.isSandbox(), request)) { + throw new AccessDeniedException("File operation is not in a permitted location"); + } else if(!allowRootDir && !Files.isDirectory(path)) { + if (!isGoodExtension(path)) { + throw new AccessDeniedException(path.toString()); + } + } + } public static Path getAbsolutePath(JSONObject params, RequestState request, boolean allowRootDir) throws JSONException, IOException { + return getAbsolutePath(params, request, allowRootDir, false); + } + + public static Path getAbsolutePath(JSONObject params, RequestState request, boolean allowRootDir, boolean createMissing) throws JSONException, IOException { FileParams fp = new FileParams(params); String commonName = request.isVerified()? escapeFileName(request.getCertName()):"UNTRUSTED"; Path path = createAbsolutePath(fp, commonName); + checkFileRequest(path, fp, request, allowRootDir); initializeRootFolder(fp, commonName); - if (!isWhiteListed(path, allowRootDir, fp.isSandbox(), request.getCertUsed())) { - throw new AccessDeniedException(path.toString()); - } - - if (!allowRootDir && !Files.isDirectory(path)) { - if (!isGoodExtension(path)) { - throw new AccessDeniedException(path.toString()); + if (createMissing) { + if (!SystemUtilities.isWindows()) { + Path resolve; + // Find existing parental directory + for(resolve = path.getParent(); !Files.exists(resolve); resolve = resolve.getParent()) { + // do nothing + } + Set permissions = Files.getPosixFilePermissions(resolve); + FileAttribute> fileAttributes = PosixFilePermissions.asFileAttribute(permissions); + Files.createDirectories(path.getParent(), fileAttributes); + } else { + Files.createDirectories(path.getParent()); } } @@ -309,8 +352,8 @@ public static boolean isGoodExtension(Path path) { * Currently hard-coded to the QZ data directory or anything provided by qz-tray.properties * e.g. %APPDATA%/qz/data or $HOME/.qz/data, etc */ - public static boolean isWhiteListed(Path path, boolean allowRootDir, boolean sandbox, Certificate cert) { - String commonName = cert.isTrusted()? escapeFileName(cert.getCommonName()):"UNTRUSTED"; + public static boolean isWhiteListed(Path path, boolean allowRootDir, boolean sandbox, RequestState request) { + String commonName = request.isVerified()? escapeFileName(request.getCertName()):"UNTRUSTED"; if (whiteList == null) { whiteList = new ArrayList<>(); //default sandbox locations. More can be added through the properties file diff --git a/src/qz/ws/PrintSocketClient.java b/src/qz/ws/PrintSocketClient.java index 4c20767a6..6140cdd18 100644 --- a/src/qz/ws/PrintSocketClient.java +++ b/src/qz/ws/PrintSocketClient.java @@ -565,7 +565,7 @@ private void processMessage(Session session, JSONObject json, SocketConnection c } case FILE_WRITE: { FileParams fileParams = new FileParams(params); - Path absPath = FileUtilities.getAbsolutePath(params, request, false); + Path absPath = FileUtilities.getAbsolutePath(params, request, false, true); Files.write(absPath, fileParams.getData(), StandardOpenOption.CREATE, fileParams.getAppendMode()); FileUtilities.inheritParentPermissions(absPath);