Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -17,7 +21,7 @@ For adding a library only:
<dependency>
<groupId>com.instancify.scriptify</groupId>
<artifactId>core</artifactId>
<version>1.3.6-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
</dependency>
```

Expand All @@ -26,12 +30,12 @@ For adding a library with JS for Rhino or GraalVM:
<dependency>
<groupId>com.instancify.scriptify</groupId>
<artifactId>script-js-rhino</artifactId>
<version>1.3.6-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.instancify.scriptify</groupId>
<artifactId>script-js-graalvm</artifactId>
<version>1.3.6-SNAPSHOT</version>
<version>1.4.0-SNAPSHOT</version>
</dependency>
```
## Gradle
Expand All @@ -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"
```
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ java {

allprojects {
group = "com.instancify.scriptify"
version = "1.3.7-SNAPSHOT"
version = "1.4.0-SNAPSHOT"
}

subprojects {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion script-js-graalvm/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion script-js-rhino/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ repositories {
}

dependencies {
api(project(":core"))
api(project(":security"))
api("org.mozilla:rhino:1.8.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
11 changes: 11 additions & 0 deletions security/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
plugins {
id("java")
}

repositories {
mavenCentral()
}

dependencies {
api(project(":api"))
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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;
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,6 +12,7 @@ public class StandardSecurityManager implements ScriptSecurityManager {
private boolean securityMode;
private final Set<SecurityExclude> excludes = new HashSet<>();
private final SecurityPathAccessor pathAccessor = new SecurityPathAccessorImpl(this);
private final SecurityFileSystemImpl fileSystem = new SecurityFileSystemImpl(pathAccessor);

@Override
public boolean getSecurityMode() {
Expand All @@ -23,6 +24,11 @@ public void setSecurityMode(boolean securityMode) {
this.securityMode = securityMode;
}

@Override
public SecurityFileSystemImpl getFileSystem() {
return fileSystem;
}

@Override
public SecurityPathAccessor getPathAccessor() {
return pathAccessor;
Expand Down
Loading