Skip to content
Open
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
41 changes: 37 additions & 4 deletions cmd/integration-test/javascript.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

var jsTestcases = []TestCaseInfo{
{Path: "protocols/javascript/redis-pass-brute.yaml", TestCase: &javascriptRedisPassBrute{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{Path: "protocols/javascript/redis-lua-script.yaml", TestCase: &javascriptRedisLuaScript{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{Path: "protocols/javascript/ssh-server-fingerprint.yaml", TestCase: &javascriptSSHServerFingerprint{}, DisableOn: func() bool { return osutils.IsWindows() || osutils.IsOSX() }},
{Path: "protocols/javascript/net-multi-step.yaml", TestCase: &networkMultiStep{}},
{Path: "protocols/javascript/net-https.yaml", TestCase: &javascriptNetHttps{}},
Expand Down Expand Up @@ -54,7 +55,39 @@ func (j *javascriptRedisPassBrute) Execute(filePath string) error {
results := []string{}
var err error
_ = pool.Retry(func() error {
//let ssh server start
// let redis server start
time.Sleep(3 * time.Second)
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
return nil
})
if err != nil {
return err
}
if err := expectResultsCount(results, 1); err == nil {
return nil
} else {
errs = append(errs, err)
}
}
return multierr.Combine(errs...)
}

type javascriptRedisLuaScript struct{}

func (j *javascriptRedisLuaScript) Execute(filePath string) error {
if redisResource == nil || pool == nil {
// skip test as redis is not running
return nil
}
tempPort := redisResource.GetPort("6379/tcp")
finalURL := "localhost:" + tempPort
defer purge(redisResource)
errs := []error{}
for i := 0; i < defaultRetry; i++ {
results := []string{}
var err error
_ = pool.Retry(func() error {
// let redis server start
time.Sleep(3 * time.Second)
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
return nil
Expand Down Expand Up @@ -86,7 +119,7 @@ func (j *javascriptSSHServerFingerprint) Execute(filePath string) error {
results := []string{}
var err error
_ = pool.Retry(func() error {
//let ssh server start
// let ssh server start
time.Sleep(3 * time.Second)
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
return nil
Expand Down Expand Up @@ -119,7 +152,7 @@ func (j *javascriptOracleAuthTest) Execute(filePath string) error {
results := []string{}
var err error
_ = pool.Retry(func() error {
//let ssh server start
// let oracle server start
time.Sleep(3 * time.Second)
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
return nil
Expand Down Expand Up @@ -151,7 +184,7 @@ func (j *javascriptVncPassBrute) Execute(filePath string) error {
results := []string{}
var err error
_ = pool.Retry(func() error {
//let ssh server start
// let vnc server start
time.Sleep(3 * time.Second)
results, err = testutils.RunNucleiTemplateAndGetResults(filePath, finalURL, debug)
return nil
Expand Down
27 changes: 27 additions & 0 deletions integration_tests/protocols/javascript/redis-lua-script.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
id: redis-lua-script

info:
name: Redis RunLuaScript - Detect
author: DhiyaneshDK
severity: info

javascript:
- code: |
const redis = require('nuclei/redis');
// First call: Set a key-value pair using ARGV
const setResult = redis.RunLuaScript(Host, Port, Password, 'return redis.call("set", ARGV[1], ARGV[2])', [], ['testkey', 'testvalue']);
log(to_json(setResult));

// Second call: Retrieve the value we just set
const getResult = redis.RunLuaScript(Host, Port, Password, 'return redis.call("get", ARGV[1])', [], ['testkey']);
log(to_json(getResult));

args:
Host: "{{Host}}"
Port: "6379"
Password: ""

matchers:
- type: dsl
dsl:
- "success == true"
7 changes: 5 additions & 2 deletions pkg/js/generated/ts/redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ export function IsAuthenticated(host: string, port: number): boolean | null {
* @example
* ```javascript
* const redis = require('nuclei/redis');
* const result = redis.RunLuaScript('acme.com', 6379, 'password', 'return redis.call("get", KEYS[1])');
* // Old signature (backwards compatible) - keys and args are optional
* const result = redis.RunLuaScript('acme.com', 6379, 'password', 'return redis.call("ping")');
* // New signature with keys and args
* const result = redis.RunLuaScript('acme.com', 6379, 'password', 'return redis.call("get", KEYS[1])', ['mykey'], []);
* ```
*/
export function RunLuaScript(host: string, port: number, password: string, script: string): any | null {
export function RunLuaScript(host: string, port: number, password: string, script: string, keys?: string[] | any, args?: string[] | any): any | null {
return null;
}

48 changes: 44 additions & 4 deletions pkg/js/libs/redis/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,12 @@ func isAuthenticated(executionId string, host string, port int) (bool, error) {
// @example
// ```javascript
// const redis = require('nuclei/redis');
// const result = redis.RunLuaScript('acme.com', 6379, 'password', 'return redis.call("get", KEYS[1])');
// // Old signature (backwards compatible) - keys and args are optional
// const result = redis.RunLuaScript('acme.com', 6379, 'password', 'return redis.call("ping")');
// // New signature with keys and args
// const result = redis.RunLuaScript('acme.com', 6379, 'password', 'return redis.call("get", KEYS[1])', ['mykey'], []);
// ```
func RunLuaScript(ctx context.Context, host string, port int, password string, script string) (interface{}, error) {
func RunLuaScript(ctx context.Context, host string, port int, password string, script string, keys interface{}, args interface{}) (interface{}, error) {
executionId := ctx.Value("executionId").(string)
if !protocolstate.IsHostAllowed(executionId, host) {
// host is not valid according to network policy
Expand All @@ -199,8 +202,45 @@ func RunLuaScript(ctx context.Context, host string, port int, password string, s
return "", err
}

// Get Redis server info
infoCmd := client.Eval(context.Background(), script, []string{})
// Convert interface{} to []string for keys (handle backwards compatibility)
keysSlice := []string{}
if keys != nil {
switch v := keys.(type) {
case []string:
keysSlice = v
case []interface{}:
keysSlice = make([]string, 0, len(v))
for _, item := range v {
keysSlice = append(keysSlice, fmt.Sprintf("%v", item))
}
}
}

// Convert interface{} to []string for args (handle backwards compatibility)
argsSlice := []string{}
if args != nil {
switch v := args.(type) {
case []string:
argsSlice = v
case []interface{}:
// Convert []interface{} to []string (from JavaScript arrays)
argsSlice = make([]string, 0, len(v))
for _, item := range v {
if s, ok := item.(string); ok {
argsSlice = append(argsSlice, s)
}
}
}
}
Comment on lines +219 to +234
Copy link
Contributor

@coderabbitai coderabbitai bot Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical inconsistency: args still silently drop non-string items.

The args conversion at lines 229-231 only keeps string items and silently drops non-strings, while the keys conversion (lines 213-215) uses fmt.Sprintf("%v", item) to preserve all types. This inconsistency will cause confusing bugs where args like ['key1', 123, true] become ['key1'], causing Lua scripts to fail.

Redis Lua scripts commonly use numeric and boolean arguments, so this silent data loss is a significant correctness issue.

Apply this diff to make args handling consistent with keys:

 	// Convert interface{} to []string for args (handle backwards compatibility)
 	argsSlice := []string{}
 	if args != nil {
 		switch v := args.(type) {
 		case []string:
 			argsSlice = v
 		case []interface{}:
 			// Convert []interface{} to []string (from JavaScript arrays)
 			argsSlice = make([]string, 0, len(v))
 			for _, item := range v {
-				if s, ok := item.(string); ok {
-					argsSlice = append(argsSlice, s)
-				}
+				argsSlice = append(argsSlice, fmt.Sprintf("%v", item))
 			}
+		default:
+			return nil, fmt.Errorf("args must be []string or []interface{}, got %T", args)
 		}
 	}

Note: This addresses the same concern raised in the past review comment about silent data loss.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In pkg/js/libs/redis/redis.go around lines 219-234 the args conversion silently
drops non-string items (only appends items asserted as string), causing
inconsistent behavior vs. keys which uses fmt.Sprintf("%v", item); change the
args handling to mirror keys: when args is []string keep as-is, when args is
[]interface{} iterate each item and append fmt.Sprintf("%v", item) so
numbers/booleans are preserved as their string representation, and ensure
args==nil remains handled.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwisiswant0, thanks for confirming! You're absolutely right that this inconsistency should be fixed.

The current implementation silently drops non-string arguments (lines 229-231), which would cause Lua scripts to fail when expecting numeric or boolean values. Since Redis Lua scripts commonly use these types as arguments, this creates a significant correctness issue.

@Mzack9999, please apply the suggested fix to make args handling consistent with keys:

Change the args conversion (lines 226-234) to:

// Convert interface{} to []string for args (handle backwards compatibility)
argsSlice := []string{}
if args != nil {
	switch v := args.(type) {
	case []string:
		argsSlice = v
	case []interface{}:
		// Convert []interface{} to []string (from JavaScript arrays)
		argsSlice = make([]string, 0, len(v))
		for _, item := range v {
			argsSlice = append(argsSlice, fmt.Sprintf("%v", item))
		}
	default:
		return nil, fmt.Errorf("args must be []string or []interface{}, got %T", args)
	}
}

This ensures that arguments like ['key1', 123, true] are properly converted to their string representations instead of being silently dropped, which aligns with how Redis EVAL expects arguments to be passed.


// Convert []string args to []interface{} for Eval
argsInterface := make([]interface{}, len(argsSlice))
for i, arg := range argsSlice {
argsInterface[i] = arg
}

// Execute the Lua script with keys and args
infoCmd := client.Eval(context.Background(), script, keysSlice, argsInterface...)

if infoCmd.Err() != nil {
return "", infoCmd.Err()
Expand Down
Loading