diff --git a/README.md b/README.md
index f1f7498..b827f3f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
# Instancify Scriptify
Instancify Scriptify is a library to evaluate scripts written in JavaScript with ability to add global functions and variables.
+## What is it for?
+This library is designed to execute JavaScript scripts and has the ability to register global functions and constants.
+It also allows you to configure security for executing scripts.
+
## Maven
Adding repo:
```xml
@@ -17,7 +21,7 @@ For adding a library only:
com.instancify.scriptify
core
- 1.3.6-SNAPSHOT
+ 1.4.0-SNAPSHOT
```
@@ -26,12 +30,12 @@ For adding a library with JS for Rhino or GraalVM:
com.instancify.scriptify
script-js-rhino
- 1.3.6-SNAPSHOT
+ 1.4.0-SNAPSHOT
com.instancify.scriptify
script-js-graalvm
- 1.3.6-SNAPSHOT
+ 1.4.0-SNAPSHOT
```
## Gradle
@@ -45,11 +49,11 @@ maven {
For adding a library only:
```groovy
-implementation "com.instancify.scriptify:core:1.3.6-SNAPSHOT"
+implementation "com.instancify.scriptify:core:1.4.0-SNAPSHOT"
```
For adding a library with JS for Rhino or GraalVM:
```groovy
-implementation "com.instancify.scriptify:script-js-rhino:1.3.6-SNAPSHOT"
-implementation "com.instancify.scriptify:script-js-graalvm:1.3.6-SNAPSHOT"
+implementation "com.instancify.scriptify:script-js-rhino:1.4.0-SNAPSHOT"
+implementation "com.instancify.scriptify:script-js-graalvm:1.4.0-SNAPSHOT"
```
\ No newline at end of file
diff --git a/api/src/main/java/com/instancify/scriptify/api/script/security/ScriptSecurityManager.java b/api/src/main/java/com/instancify/scriptify/api/script/security/ScriptSecurityManager.java
index ed6c3bd..076fa16 100644
--- a/api/src/main/java/com/instancify/scriptify/api/script/security/ScriptSecurityManager.java
+++ b/api/src/main/java/com/instancify/scriptify/api/script/security/ScriptSecurityManager.java
@@ -22,6 +22,13 @@ public interface ScriptSecurityManager {
*/
void setSecurityMode(boolean securityMode);
+ /**
+ * Receives security file system.
+ *
+ * @return Security file system
+ */
+ SecurityFileSystem getFileSystem();
+
/**
* Receives security path accessor.
*
diff --git a/api/src/main/java/com/instancify/scriptify/api/script/security/SecurityFileSystem.java b/api/src/main/java/com/instancify/scriptify/api/script/security/SecurityFileSystem.java
new file mode 100644
index 0000000..ab79ddd
--- /dev/null
+++ b/api/src/main/java/com/instancify/scriptify/api/script/security/SecurityFileSystem.java
@@ -0,0 +1,34 @@
+package com.instancify.scriptify.api.script.security;
+
+import java.io.File;
+import java.nio.file.Path;
+
+/**
+ * Provides secure access to files and paths, ensuring
+ * all operations are restricted to the configured base path.
+ */
+public interface SecurityFileSystem {
+
+ /**
+ * Returns a secure Path inside the base path.
+ *
+ * @param path the path string to resolve
+ * @return a normalized and secure Path
+ * @throws SecurityException if the path is outside basePath or not accessible
+ */
+ Path getPath(String path);
+
+ /**
+ * Returns a secure File inside the base path.
+ *
+ * @param path the path string to resolve
+ * @return a normalized and secure File
+ * @throws SecurityException if the path is outside basePath or not accessible
+ */
+ File getFile(String path);
+
+ /**
+ * Returns the base path for this file system.
+ */
+ Path getBasePath();
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index 9ac26cf..f856ba6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -12,7 +12,7 @@ java {
allprojects {
group = "com.instancify.scriptify"
- version = "1.3.7-SNAPSHOT"
+ version = "1.4.0-SNAPSHOT"
}
subprojects {
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDeleteFile.java b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDeleteFile.java
index 0ded336..36bd64e 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDeleteFile.java
+++ b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDeleteFile.java
@@ -30,14 +30,14 @@ public Object invoke(Script> script, ScriptFunctionArgument[] args) throws Scr
}
if (args.length == 1) {
- return new File(filePath).delete();
+ return script.getSecurityManager().getFileSystem().getFile(filePath).delete();
}
if (!(args[1].getValue() instanceof Boolean recursive)) {
throw new ScriptFunctionArgTypeException(Boolean.class, args[1].getType());
}
- File file = new File(filePath);
+ File file = script.getSecurityManager().getFileSystem().getFile(filePath);
if (recursive) {
return deleteDirectoryRecursively(file);
} else {
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDownloadFromUrl.java b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDownloadFromUrl.java
index d659cf1..92a8ed9 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDownloadFromUrl.java
+++ b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionDownloadFromUrl.java
@@ -11,7 +11,8 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
-import java.net.URL;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.nio.file.Files;
/**
@@ -37,11 +38,11 @@ public Object invoke(Script> script, ScriptFunctionArgument[] args) throws Scr
throw new ScriptFunctionArgTypeException(String.class, args[1].getType());
}
- try (InputStream in = new URL(url).openStream()) {
- File targetPath = new File(filePath);
+ try (InputStream in = new URI(url).toURL().openStream()) {
+ File targetPath = script.getSecurityManager().getFileSystem().getFile(filePath);
Files.copy(in, targetPath.toPath());
- } catch (IOException e) {
- e.printStackTrace();
+ } catch (IOException | URISyntaxException e) {
+ throw new RuntimeException(e);
}
return null;
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionExistsFile.java b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionExistsFile.java
index 41f5726..4d81a93 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionExistsFile.java
+++ b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionExistsFile.java
@@ -25,7 +25,7 @@ public class ScriptFunctionExistsFile implements ScriptFunction {
public Object invoke(Script> script, ScriptFunctionArgument[] args) throws ScriptFunctionException {
if (args.length == 1) {
if (args[0].getValue() instanceof String filePath) {
- return Files.exists(Path.of(filePath));
+ return Files.exists(script.getSecurityManager().getFileSystem().getPath(filePath));
} else {
throw new ScriptFunctionArgTypeException(String.class, args[0].getType());
}
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionListFiles.java b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionListFiles.java
index 14ceb6e..bc0477c 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionListFiles.java
+++ b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionListFiles.java
@@ -9,8 +9,8 @@
import org.jetbrains.annotations.NotNull;
import java.io.File;
-import java.nio.file.Paths;
import java.util.Arrays;
+import java.util.Objects;
/**
* Represents a function to get all files in a folder
@@ -26,9 +26,9 @@ public class ScriptFunctionListFiles implements ScriptFunction {
public Object invoke(Script> script, ScriptFunctionArgument[] args) throws ScriptFunctionException {
if (args.length == 1) {
if (args[0].getValue() instanceof String filePath) {
- File folder = Paths.get(filePath).toAbsolutePath().toFile();
+ File folder = script.getSecurityManager().getFileSystem().getFile(filePath);
if (folder.isDirectory()) {
- return Arrays.stream(folder.listFiles()).map(File::getAbsolutePath).toList();
+ return Arrays.stream(Objects.requireNonNull(folder.listFiles())).map(File::getAbsolutePath).toList();
} else {
throw new ScriptFunctionException("File is not a folder");
}
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionMoveFile.java b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionMoveFile.java
index 3174c48..3a4dc36 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionMoveFile.java
+++ b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionMoveFile.java
@@ -33,7 +33,7 @@ public Object invoke(Script> script, ScriptFunctionArgument[] args) throws Scr
throw new ScriptFunctionArgTypeException(String.class, args[1].getType());
}
- File fileToMove = new File(originalFilePath);
- return fileToMove.renameTo(new File(targetFilePath));
+ File fileToMove = script.getSecurityManager().getFileSystem().getFile(originalFilePath);
+ return fileToMove.renameTo(script.getSecurityManager().getFileSystem().getFile(targetFilePath));
}
}
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionReadFile.java b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionReadFile.java
index 7bc6261..abcaa45 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionReadFile.java
+++ b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionReadFile.java
@@ -27,7 +27,7 @@ public Object invoke(Script> script, ScriptFunctionArgument[] args) throws Scr
if (args.length == 1) {
if (args[0].getValue() instanceof String filePath) {
try {
- return Files.readString(Path.of(filePath));
+ return Files.readString(script.getSecurityManager().getFileSystem().getPath(filePath));
} catch (IOException e) {
throw new ScriptFunctionException(e);
}
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionWriteFile.java b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionWriteFile.java
index 8b84678..f2f2b60 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionWriteFile.java
+++ b/core/src/main/java/com/instancify/scriptify/core/script/function/impl/file/ScriptFunctionWriteFile.java
@@ -26,7 +26,7 @@ public Object invoke(Script> script, ScriptFunctionArgument[] args) throws Scr
if (args.length == 2) {
if (args[0].getValue() instanceof String filePath && args[1].getValue() instanceof String fileContent) {
try {
- return Files.writeString(script.getSecurityManager().getPathAccessor().getAccessiblePath(filePath), fileContent);
+ return Files.writeString(script.getSecurityManager().getFileSystem().getPath(filePath), fileContent);
} catch (IOException e) {
throw new ScriptFunctionException(e);
}
diff --git a/script-js-graalvm/build.gradle.kts b/script-js-graalvm/build.gradle.kts
index 6c13dec..05daa86 100644
--- a/script-js-graalvm/build.gradle.kts
+++ b/script-js-graalvm/build.gradle.kts
@@ -7,7 +7,7 @@ repositories {
}
dependencies {
- api(project(":core"))
+ api(project(":security"))
api("org.graalvm.polyglot:polyglot:24.1.1")
api("org.graalvm.polyglot:js:24.1.1")
}
\ No newline at end of file
diff --git a/script-js-graalvm/src/main/java/com/instancify/scriptify/script/JsScript.java b/script-js-graalvm/src/main/java/com/instancify/scriptify/script/JsScript.java
index 45e96c1..a63beb6 100644
--- a/script-js-graalvm/src/main/java/com/instancify/scriptify/script/JsScript.java
+++ b/script-js-graalvm/src/main/java/com/instancify/scriptify/script/JsScript.java
@@ -8,7 +8,7 @@
import com.instancify.scriptify.api.script.function.ScriptFunction;
import com.instancify.scriptify.api.script.function.ScriptFunctionManager;
import com.instancify.scriptify.api.script.security.ScriptSecurityManager;
-import com.instancify.scriptify.core.script.security.StandardSecurityManager;
+import com.instancify.scriptify.security.StandardSecurityManager;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.Value;
diff --git a/script-js-rhino/build.gradle.kts b/script-js-rhino/build.gradle.kts
index 0e1519b..3a03bcf 100644
--- a/script-js-rhino/build.gradle.kts
+++ b/script-js-rhino/build.gradle.kts
@@ -7,6 +7,6 @@ repositories {
}
dependencies {
- api(project(":core"))
+ api(project(":security"))
api("org.mozilla:rhino:1.8.0")
}
\ No newline at end of file
diff --git a/script-js-rhino/src/main/java/com/instancify/scriptify/script/JsScript.java b/script-js-rhino/src/main/java/com/instancify/scriptify/script/JsScript.java
index d8dffaa..a9943ea 100644
--- a/script-js-rhino/src/main/java/com/instancify/scriptify/script/JsScript.java
+++ b/script-js-rhino/src/main/java/com/instancify/scriptify/script/JsScript.java
@@ -7,7 +7,7 @@
import com.instancify.scriptify.api.script.function.ScriptFunction;
import com.instancify.scriptify.api.script.function.ScriptFunctionManager;
import com.instancify.scriptify.api.script.security.ScriptSecurityManager;
-import com.instancify.scriptify.core.script.security.StandardSecurityManager;
+import com.instancify.scriptify.security.StandardSecurityManager;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ScriptableObject;
diff --git a/security/build.gradle.kts b/security/build.gradle.kts
new file mode 100644
index 0000000..70f5c43
--- /dev/null
+++ b/security/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id("java")
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ api(project(":api"))
+}
\ No newline at end of file
diff --git a/security/src/main/java/com/instancify/scriptify/security/SecurityFileSystemImpl.java b/security/src/main/java/com/instancify/scriptify/security/SecurityFileSystemImpl.java
new file mode 100644
index 0000000..fe963b1
--- /dev/null
+++ b/security/src/main/java/com/instancify/scriptify/security/SecurityFileSystemImpl.java
@@ -0,0 +1,31 @@
+package com.instancify.scriptify.security;
+
+import com.instancify.scriptify.api.script.security.SecurityFileSystem;
+import com.instancify.scriptify.api.script.security.SecurityPathAccessor;
+
+import java.io.File;
+import java.nio.file.Path;
+
+public class SecurityFileSystemImpl implements SecurityFileSystem {
+
+ private final SecurityPathAccessor pathAccessor;
+
+ public SecurityFileSystemImpl(SecurityPathAccessor pathAccessor) {
+ this.pathAccessor = pathAccessor;
+ }
+
+ @Override
+ public Path getPath(String path) {
+ return pathAccessor.getAccessiblePath(path);
+ }
+
+ @Override
+ public File getFile(String path) {
+ return this.getPath(path).toFile();
+ }
+
+ @Override
+ public Path getBasePath() {
+ return pathAccessor.getBasePath();
+ }
+}
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/security/SecurityPathAccessorImpl.java b/security/src/main/java/com/instancify/scriptify/security/SecurityPathAccessorImpl.java
similarity index 61%
rename from core/src/main/java/com/instancify/scriptify/core/script/security/SecurityPathAccessorImpl.java
rename to security/src/main/java/com/instancify/scriptify/security/SecurityPathAccessorImpl.java
index 3fd1bfd..3aea9d8 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/security/SecurityPathAccessorImpl.java
+++ b/security/src/main/java/com/instancify/scriptify/security/SecurityPathAccessorImpl.java
@@ -1,4 +1,4 @@
-package com.instancify.scriptify.core.script.security;
+package com.instancify.scriptify.security;
import com.instancify.scriptify.api.script.security.ScriptSecurityManager;
import com.instancify.scriptify.api.script.security.SecurityPathAccessor;
@@ -57,18 +57,31 @@ public void setBasePath(Path basePath) {
}
/**
- * Returns a path that is safe to access according to security rules. If the path is not accessible,
- * it returns a path relative to the base path with ':' characters removed to prevent potential path traversal attacks.
+ * Returns a path that is safe to access according to security rules.
+ * If the path is accessible via exclusions, returns the normalized path.
+ * If the path is not accessible, creates a safe path within basePath by cleaning the path from invalid characters.
*
* @param path The path string to be checked and possibly modified
- * @return A Path object representing the accessible path or a sanitized version if not accessible
+ * @return A Path object representing the accessible path or a path within base directory
*/
@Override
public Path getAccessiblePath(String path) {
if (this.isAccessible(path)) {
- return Path.of(path);
+ // Path is in exclusions - return it normalized
+ return Paths.get(path).normalize().toAbsolutePath();
}
- return Path.of(basePath.toString(), path.replaceAll(":", ""));
+
+ // Path is not accessible - create safe path within basePath
+ // We need to manually combine paths because resolve() ignores basePath for absolute paths
+ Path safePath = Paths.get(basePath.toString(), path.replace(":", "")).normalize();
+
+ // CRITICAL: Ensure the result stays within basePath boundaries
+ if (!safePath.startsWith(basePath)) {
+ // If path tries to escape basePath (e.g., "../"), return basePath itself
+ return basePath;
+ }
+
+ return safePath;
}
/**
@@ -83,12 +96,35 @@ public boolean isAccessible(String path) {
return true;
}
+ // Normalize the path to resolve .. and . components to prevent path traversal
+ Path normalizedPath;
+ try {
+ normalizedPath = Paths.get(path).normalize().toAbsolutePath();
+ } catch (Exception e) {
+ return false;
+ }
+
+ // Check both original and normalized path against exclusions for compatibility
+ String normalizedPathString = normalizedPath.toString();
+
// Search all exclusions and check that the path is excluded
for (SecurityExclude exclude : securityManager.getExcludes()) {
if (exclude instanceof PathSecurityExclude) {
+ // Check original path first
if (exclude.isExcluded(path)) {
return true;
}
+
+ // Check normalized path
+ if (exclude.isExcluded(normalizedPathString)) {
+ return true;
+ }
+
+ // Check with forward slashes for cross-platform compatibility
+ String pathWithForwardSlashes = normalizedPathString.replace('\\', '/');
+ if (exclude.isExcluded(pathWithForwardSlashes)) {
+ return true;
+ }
}
}
diff --git a/core/src/main/java/com/instancify/scriptify/core/script/security/StandardSecurityManager.java b/security/src/main/java/com/instancify/scriptify/security/StandardSecurityManager.java
similarity index 82%
rename from core/src/main/java/com/instancify/scriptify/core/script/security/StandardSecurityManager.java
rename to security/src/main/java/com/instancify/scriptify/security/StandardSecurityManager.java
index 20dbb66..cf5e971 100644
--- a/core/src/main/java/com/instancify/scriptify/core/script/security/StandardSecurityManager.java
+++ b/security/src/main/java/com/instancify/scriptify/security/StandardSecurityManager.java
@@ -1,4 +1,4 @@
-package com.instancify.scriptify.core.script.security;
+package com.instancify.scriptify.security;
import com.instancify.scriptify.api.script.security.ScriptSecurityManager;
import com.instancify.scriptify.api.script.security.SecurityPathAccessor;
@@ -12,6 +12,7 @@ public class StandardSecurityManager implements ScriptSecurityManager {
private boolean securityMode;
private final Set excludes = new HashSet<>();
private final SecurityPathAccessor pathAccessor = new SecurityPathAccessorImpl(this);
+ private final SecurityFileSystemImpl fileSystem = new SecurityFileSystemImpl(pathAccessor);
@Override
public boolean getSecurityMode() {
@@ -23,6 +24,11 @@ public void setSecurityMode(boolean securityMode) {
this.securityMode = securityMode;
}
+ @Override
+ public SecurityFileSystemImpl getFileSystem() {
+ return fileSystem;
+ }
+
@Override
public SecurityPathAccessor getPathAccessor() {
return pathAccessor;
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 1c1358b..df8de18 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,6 +1,7 @@
rootProject.name = "instancify-Scriptify"
include("api")
include("core")
+include("security")
include("script-js-graalvm")
include("script-js-rhino")
include("http")