diff --git a/README.md b/README.md
index 6961861..07d4347 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,9 @@ When defining the script, you can specify one of the following placeholders that
When no placeholder was specified, then the exact command as given is being executed.
+### Security
+Do not wrap placeholders in quotes, as they are already quoted by the app. Wrapping them in quotes can neutralize this and may allow command execution via filenames. Files containing `$(` or `` ` `` in their filename will be skipped.
+
### Hints
Events for files and folders are triggered by file system operations. An operation like
diff --git a/lib/BackgroundJobs/Launcher.php b/lib/BackgroundJobs/Launcher.php
index 71a2e1f..f7714e8 100644
--- a/lib/BackgroundJobs/Launcher.php
+++ b/lib/BackgroundJobs/Launcher.php
@@ -8,6 +8,7 @@
namespace OCA\WorkflowScript\BackgroundJobs;
use Exception;
+use InvalidArgumentException;
use OC\Files\View;
use OCA\WorkflowScript\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
@@ -35,8 +36,7 @@ protected function run($argument): void {
if (strpos($command, '%f')) {
$path = isset($argument['path']) ? (string)$argument['path'] : '';
try {
- $view = new View(dirname($path));
- $tmpFile = $view->toTmpFile(basename($path));
+ $command = str_replace('%f', escapeshellarg($this->resolveLocalPath($path)), $command);
} catch (Exception $e) {
$this->logger->warning($e->getMessage(), [
'app' => Application::APPID,
@@ -44,7 +44,6 @@ protected function run($argument): void {
]);
return;
}
- $command = str_replace('%f', escapeshellarg($tmpFile), $command);
}
// with wrapping sh around the command, we leave any redirects intact,
@@ -56,4 +55,24 @@ protected function run($argument): void {
);
shell_exec($wrapper);
}
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ private function resolveLocalPath(string $path): string {
+ try {
+ $view = new View();
+ $localFile = $view->getLocalFile($path);
+ if ($localFile !== false && file_exists($localFile)) {
+ return $localFile;
+ }
+ $tmpFile = $view->toTmpFile($path);
+ if ($tmpFile === false) {
+ throw new InvalidArgumentException();
+ }
+ return $tmpFile;
+ } catch (Exception) {
+ throw new InvalidArgumentException('Could not resolve local path for: ' . $path);
+ }
+ }
}
diff --git a/lib/Operation.php b/lib/Operation.php
index 681279a..81f8957 100644
--- a/lib/Operation.php
+++ b/lib/Operation.php
@@ -7,9 +7,6 @@
namespace OCA\WorkflowScript;
-use Exception;
-use InvalidArgumentException;
-use OC\Files\View;
use OC\User\NoUserException;
use OCA\Files_Sharing\SharedStorage;
use OCA\GroupFolders\Mount\GroupFolderStorage;
@@ -129,6 +126,21 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch
}
$matches = $ruleMatcher->getFlows(false);
+ if (empty($matches)) {
+ return;
+ }
+
+ if (preg_match('/\$\(|`/', $node->getName())) {
+ $this->logger->warning(
+ 'Potentially dangerous characters in filename, skipping workflow',
+ [
+ 'app' => Application::APPID,
+ 'file' => $node->getPath(),
+ ]
+ );
+ return;
+ }
+
foreach ($matches as $match) {
try {
$command = $this->buildCommand($match['operation'], $node, $eventName, $extra);
@@ -172,21 +184,6 @@ protected function buildCommand(string $template, Node $node, string $event, arr
unset($ncRelPath);
}
- if (strpos($command, '%f')) {
- try {
- $view = new View();
- if ($node instanceof FileNode) {
- $fullPath = $view->getLocalFile($node->getPath());
- }
- if (!isset($fullPath) || $fullPath === false) {
- throw new InvalidArgumentException();
- }
- $command = str_replace('%f', escapeshellarg($fullPath), $command);
- } catch (Exception) {
- throw new InvalidArgumentException('Could not determine full path');
- }
- }
-
if (strpos($command, '%i')) {
$nodeID = -1;
try {
diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index 6c2cb52..55bca5a 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -17,7 +17,6 @@
-