mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-24 20:28:06 +02:00
[pitboss] phase 05: M5 — JS/TS, Go, Java, PHP harness emitters
This commit is contained in:
parent
84638e7d57
commit
345b44d3cc
103 changed files with 5637 additions and 34 deletions
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "nyx/escape-composer-postinstall",
|
||||
"description": "Malicious Composer post-install-cmd escape fixture.",
|
||||
"require": {},
|
||||
"scripts": {
|
||||
"post-install-cmd": [
|
||||
"echo NYX_ESCAPE_SUCCESS > /tmp/pwned_composer_postinstall"
|
||||
]
|
||||
}
|
||||
}
|
||||
16
tests/dynamic_fixtures/escape/go_malicious_init.go
Normal file
16
tests/dynamic_fixtures/escape/go_malicious_init.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// Malicious Go init() escape fixture.
|
||||
//
|
||||
// init() runs automatically before the entry point when the binary starts.
|
||||
// Expected: Docker sandbox prevents the write from reaching the host filesystem.
|
||||
// Host marker: /tmp/pwned_go_init
|
||||
// Expected verdict: marker absent on host after sandbox run.
|
||||
package entry
|
||||
|
||||
import "os"
|
||||
|
||||
func init() {
|
||||
// Escape attempt: write a marker file to a path outside the workdir.
|
||||
_ = os.WriteFile("/tmp/pwned_go_init", []byte("NYX_ESCAPE_SUCCESS\n"), 0644)
|
||||
}
|
||||
|
||||
func Login(username string) {}
|
||||
40
tests/dynamic_fixtures/escape/maven_malicious_plugin/pom.xml
Normal file
40
tests/dynamic_fixtures/escape/maven_malicious_plugin/pom.xml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Malicious Maven plugin escape fixture.
|
||||
The exec-maven-plugin runs a shell command during the validate phase of mvn compile.
|
||||
Expected: Docker-isolated Maven build prevents the write from reaching the host.
|
||||
Host marker: /tmp/pwned_maven_plugin
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.nyx.escape</groupId>
|
||||
<artifactId>malicious-plugin-test</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>escape-attempt</id>
|
||||
<phase>validate</phase>
|
||||
<goals>
|
||||
<goal>exec</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<executable>sh</executable>
|
||||
<arguments>
|
||||
<argument>-c</argument>
|
||||
<argument>echo NYX_ESCAPE_SUCCESS > /tmp/pwned_maven_plugin</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "nyx-escape-npm-lifecycle",
|
||||
"version": "1.0.0",
|
||||
"description": "Malicious npm lifecycle escape fixture — preinstall runs during npm install.",
|
||||
"scripts": {
|
||||
"preinstall": "echo NYX_ESCAPE_SUCCESS > /tmp/pwned_npm_lifecycle"
|
||||
}
|
||||
}
|
||||
15
tests/dynamic_fixtures/go/cmdi_adversarial.go
Normal file
15
tests/dynamic_fixtures/go/cmdi_adversarial.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Command injection — adversarial collision fixture.
|
||||
// Prints NYX_PWN_CMDI unconditionally without reaching a command sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: RunPing(host string) Cap: CODE_EXEC
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func RunPing(host string) {
|
||||
// Coincidental oracle match — not a shell sink.
|
||||
fmt.Println("NYX_PWN_CMDI")
|
||||
_ = len(host)
|
||||
}
|
||||
18
tests/dynamic_fixtures/go/cmdi_negative.go
Normal file
18
tests/dynamic_fixtures/go/cmdi_negative.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Command injection — negative fixture.
|
||||
// Safe: passes host as a separate arg to exec.Command (no shell invoked).
|
||||
// Entry: RunPing(host string) Cap: CODE_EXEC
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func RunPing(host string) {
|
||||
// exec.Command does not invoke a shell; host is a literal argument.
|
||||
cmd := exec.Command("echo", "hello", host)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
}
|
||||
18
tests/dynamic_fixtures/go/cmdi_positive.go
Normal file
18
tests/dynamic_fixtures/go/cmdi_positive.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Command injection — positive fixture.
|
||||
// Vulnerable: passes user input to /bin/sh -c.
|
||||
// Entry: RunPing(host string) Cap: CODE_EXEC
|
||||
// Expected verdict: Confirmed ("; echo NYX_PWN_CMDI" echoes the marker)
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func RunPing(host string) {
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
cmd := exec.Command("sh", "-c", "echo hello "+host)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
}
|
||||
15
tests/dynamic_fixtures/go/cmdi_unsupported.go
Normal file
15
tests/dynamic_fixtures/go/cmdi_unsupported.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Command injection — unsupported fixture.
|
||||
// Entry is a method on a struct.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Entry: Runner.Execute Cap: CODE_EXEC
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
package entry
|
||||
|
||||
import "os/exec"
|
||||
|
||||
type Runner struct{}
|
||||
|
||||
func (r *Runner) Execute(cmd string) {
|
||||
exec.Command("sh", "-c", cmd).Run()
|
||||
}
|
||||
15
tests/dynamic_fixtures/go/fileio_adversarial.go
Normal file
15
tests/dynamic_fixtures/go/fileio_adversarial.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// File I/O — adversarial collision fixture.
|
||||
// Prints "root:" unconditionally without reading any file
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: ReadFile(userPath string) Cap: FILE_IO
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func ReadFile(userPath string) {
|
||||
// Coincidental oracle match — not a file read sink.
|
||||
fmt.Println("root: present")
|
||||
_ = len(userPath)
|
||||
}
|
||||
34
tests/dynamic_fixtures/go/fileio_negative.go
Normal file
34
tests/dynamic_fixtures/go/fileio_negative.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// File I/O — negative fixture.
|
||||
// Safe: path is resolved and validated against base directory.
|
||||
// Entry: ReadFile(userPath string) Cap: FILE_IO
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const baseDir = "/var/data"
|
||||
|
||||
func ReadFile(userPath string) {
|
||||
resolved, err := filepath.Abs(filepath.Join(baseDir, userPath))
|
||||
if err != nil || !strings.HasPrefix(resolved, baseDir+string(filepath.Separator)) {
|
||||
fmt.Println("Access denied")
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(resolved)
|
||||
if err == nil {
|
||||
fmt.Print(string(data[:min(len(data), 100)]))
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
21
tests/dynamic_fixtures/go/fileio_positive.go
Normal file
21
tests/dynamic_fixtures/go/fileio_positive.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// File I/O — positive fixture.
|
||||
// Vulnerable: reads file at user-controlled path without sanitization.
|
||||
// Entry: ReadFile(userPath string) Cap: FILE_IO
|
||||
// Expected verdict: Confirmed (../../../../etc/passwd → "root:" in output)
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func ReadFile(userPath string) {
|
||||
filePath := filepath.Join("/var/data", userPath)
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err == nil {
|
||||
fmt.Print(string(data))
|
||||
}
|
||||
}
|
||||
21
tests/dynamic_fixtures/go/fileio_unsupported.go
Normal file
21
tests/dynamic_fixtures/go/fileio_unsupported.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// File I/O — unsupported fixture.
|
||||
// Entry is a method on a struct.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Entry: FileServer.Serve Cap: FILE_IO
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type FileServer struct{ BaseDir string }
|
||||
|
||||
func (s *FileServer) Serve(path string) {
|
||||
data, err := os.ReadFile(s.BaseDir + "/" + path)
|
||||
if err == nil {
|
||||
fmt.Print(string(data))
|
||||
}
|
||||
}
|
||||
15
tests/dynamic_fixtures/go/sqli_adversarial.go
Normal file
15
tests/dynamic_fixtures/go/sqli_adversarial.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// SQL injection — adversarial collision fixture.
|
||||
// Prints NYX_SQL_CONFIRMED unconditionally without reaching a SQL sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: Login(username string) Cap: SQL_QUERY
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Login(username string) {
|
||||
// Coincidental oracle match — not a SQL sink.
|
||||
fmt.Println("NYX_SQL_CONFIRMED")
|
||||
_ = len(username)
|
||||
}
|
||||
14
tests/dynamic_fixtures/go/sqli_negative.go
Normal file
14
tests/dynamic_fixtures/go/sqli_negative.go
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// SQL injection — negative fixture.
|
||||
// Safe: uses a parameterized query; payload is a bound argument, not concatenated.
|
||||
// Entry: Login(username string) Cap: SQL_QUERY
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Login(username string) {
|
||||
template := "SELECT name FROM users WHERE name = ?"
|
||||
// Simulate parameterized execution: template is fixed.
|
||||
fmt.Println("Executing:", template, "with param length:", len(username))
|
||||
}
|
||||
15
tests/dynamic_fixtures/go/sqli_positive.go
Normal file
15
tests/dynamic_fixtures/go/sqli_positive.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// SQL injection — positive fixture.
|
||||
// Vulnerable: directly concatenates user input into SQL query string.
|
||||
// Entry: Login(username string) Cap: SQL_QUERY
|
||||
// Expected verdict: Confirmed (UNION payload echoes NYX_SQL_CONFIRMED)
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func Login(username string) {
|
||||
query := "SELECT name FROM users WHERE name='" + username + "'"
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
// Error-based echo: output the query so UNION payload is visible.
|
||||
fmt.Print("DB query: " + query + "\n")
|
||||
}
|
||||
16
tests/dynamic_fixtures/go/sqli_unsupported.go
Normal file
16
tests/dynamic_fixtures/go/sqli_unsupported.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// SQL injection — unsupported fixture.
|
||||
// Entry is a method on a struct — entry kind unsupported (only Function supported).
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Entry: UserRepo.FindUser Cap: SQL_QUERY
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
type UserRepo struct{}
|
||||
|
||||
func (r *UserRepo) FindUser(name string) {
|
||||
query := "SELECT * FROM users WHERE name='" + name + "'"
|
||||
fmt.Println(query)
|
||||
}
|
||||
15
tests/dynamic_fixtures/go/ssrf_adversarial.go
Normal file
15
tests/dynamic_fixtures/go/ssrf_adversarial.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// SSRF — adversarial collision fixture.
|
||||
// Prints "daemon:" unconditionally without making any HTTP request
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: FetchURL(targetURL string) Cap: SSRF
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func FetchURL(targetURL string) {
|
||||
// Coincidental oracle match — not an HTTP sink.
|
||||
fmt.Println("daemon: present")
|
||||
_ = len(targetURL)
|
||||
}
|
||||
34
tests/dynamic_fixtures/go/ssrf_negative.go
Normal file
34
tests/dynamic_fixtures/go/ssrf_negative.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// SSRF — negative fixture.
|
||||
// Safe: only allows http/https scheme; file:// rejected.
|
||||
// Entry: FetchURL(targetURL string) Cap: SSRF
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func FetchURL(targetURL string) {
|
||||
parsed, err := url.Parse(targetURL)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
fmt.Println("Scheme not allowed:", parsed.Scheme)
|
||||
return
|
||||
}
|
||||
resp, err := http.Get(targetURL)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Print(string(body[:min(len(body), 64)]))
|
||||
}
|
||||
}
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
33
tests/dynamic_fixtures/go/ssrf_positive.go
Normal file
33
tests/dynamic_fixtures/go/ssrf_positive.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SSRF — positive fixture.
|
||||
// Vulnerable: makes a request to a user-controlled URL.
|
||||
// Entry: FetchURL(targetURL string) Cap: SSRF
|
||||
// Expected verdict: Confirmed (file:///etc/passwd → "daemon:" in output)
|
||||
// Note: Go http.Get does not support file:// scheme; we detect it and use os.ReadFile.
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func FetchURL(targetURL string) {
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
if strings.HasPrefix(targetURL, "file://") {
|
||||
path := strings.TrimPrefix(targetURL, "file://")
|
||||
data, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
fmt.Print(string(data))
|
||||
}
|
||||
return
|
||||
}
|
||||
resp, err := http.Get(targetURL)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Print(string(body))
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/go/ssrf_unsupported.go
Normal file
20
tests/dynamic_fixtures/go/ssrf_unsupported.go
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// SSRF — unsupported fixture.
|
||||
// Entry is a method on a struct; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type HTTPClient struct{}
|
||||
|
||||
func (c *HTTPClient) Fetch(targetURL string) {
|
||||
resp, err := http.Get(targetURL)
|
||||
if err == nil {
|
||||
defer resp.Body.Close()
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
}
|
||||
}
|
||||
15
tests/dynamic_fixtures/go/xss_adversarial.go
Normal file
15
tests/dynamic_fixtures/go/xss_adversarial.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// XSS — adversarial collision fixture.
|
||||
// Prints the XSS oracle marker unconditionally without rendering any template
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: RenderPage(userInput string) Cap: HTML_ESCAPE
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func RenderPage(userInput string) {
|
||||
// Coincidental oracle match — not an HTML render sink.
|
||||
fmt.Println("<script>NYX_XSS_CONFIRMED</script>")
|
||||
_ = len(userInput)
|
||||
}
|
||||
16
tests/dynamic_fixtures/go/xss_negative.go
Normal file
16
tests/dynamic_fixtures/go/xss_negative.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
// XSS — negative fixture.
|
||||
// Safe: uses html.EscapeString before output.
|
||||
// Entry: RenderPage(userInput string) Cap: HTML_ESCAPE
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
)
|
||||
|
||||
func RenderPage(userInput string) {
|
||||
safe := html.EscapeString(userInput)
|
||||
fmt.Print("<html><body>" + safe + "</body></html>\n")
|
||||
}
|
||||
13
tests/dynamic_fixtures/go/xss_positive.go
Normal file
13
tests/dynamic_fixtures/go/xss_positive.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// XSS — positive fixture.
|
||||
// Vulnerable: echoes raw user input into HTML output without escaping.
|
||||
// Entry: RenderPage(userInput string) Cap: HTML_ESCAPE
|
||||
// Expected verdict: Confirmed (<script>NYX_XSS_CONFIRMED</script> echoed)
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
func RenderPage(userInput string) {
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
fmt.Print("<html><body>" + userInput + "</body></html>\n")
|
||||
}
|
||||
13
tests/dynamic_fixtures/go/xss_unsupported.go
Normal file
13
tests/dynamic_fixtures/go/xss_unsupported.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// XSS — unsupported fixture.
|
||||
// Entry is a method on a struct; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
package entry
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Renderer struct{}
|
||||
|
||||
func (r *Renderer) Render(input string) {
|
||||
fmt.Print("<html><body>" + input + "</body></html>\n")
|
||||
}
|
||||
13
tests/dynamic_fixtures/java/cmdi_adversarial.java
Normal file
13
tests/dynamic_fixtures/java/cmdi_adversarial.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Command injection — adversarial collision fixture.
|
||||
// Prints NYX_PWN_CMDI unconditionally without reaching a command sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: Entry.runPing(String) Cap: CODE_EXEC
|
||||
|
||||
public class Entry {
|
||||
public static void runPing(String host) {
|
||||
// Coincidental oracle match — not a shell sink.
|
||||
System.out.println("NYX_PWN_CMDI");
|
||||
int x = host.length();
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/java/cmdi_negative.java
Normal file
20
tests/dynamic_fixtures/java/cmdi_negative.java
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Command injection — negative fixture.
|
||||
// Safe: exec with args array; no shell; semicolons are inert.
|
||||
// Entry: Entry.runPing(String) Cap: CODE_EXEC
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class Entry {
|
||||
public static void runPing(String host) throws Exception {
|
||||
// Array form: each element is a literal argument — no shell expansion.
|
||||
String[] cmd = {"echo", "hello", host};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/java/cmdi_positive.java
Normal file
20
tests/dynamic_fixtures/java/cmdi_positive.java
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Command injection — positive fixture.
|
||||
// Vulnerable: passes user input to /bin/sh -c via Runtime.exec.
|
||||
// Entry: Entry.runPing(String) Cap: CODE_EXEC
|
||||
// Expected verdict: Confirmed ("; echo NYX_PWN_CMDI" echoes the marker)
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class Entry {
|
||||
public static void runPing(String host) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
String[] cmd = {"/bin/sh", "-c", "echo hello " + host};
|
||||
Process p = Runtime.getRuntime().exec(cmd);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
p.waitFor();
|
||||
}
|
||||
}
|
||||
11
tests/dynamic_fixtures/java/cmdi_unsupported.java
Normal file
11
tests/dynamic_fixtures/java/cmdi_unsupported.java
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Command injection — unsupported fixture.
|
||||
// Entry is an instance method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
import java.io.*;
|
||||
|
||||
public class Entry {
|
||||
public void execute(String cmd) throws Exception {
|
||||
Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", cmd});
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/java/fileio_adversarial.java
Normal file
13
tests/dynamic_fixtures/java/fileio_adversarial.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// File I/O — adversarial collision fixture.
|
||||
// Prints "root:" unconditionally without reading any file
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: Entry.readFile(String) Cap: FILE_IO
|
||||
|
||||
public class Entry {
|
||||
public static void readFile(String userPath) {
|
||||
// Coincidental oracle match — not a file read sink.
|
||||
System.out.println("root: present");
|
||||
int x = userPath.length();
|
||||
}
|
||||
}
|
||||
27
tests/dynamic_fixtures/java/fileio_negative.java
Normal file
27
tests/dynamic_fixtures/java/fileio_negative.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// File I/O — negative fixture.
|
||||
// Safe: normalizes path and checks it stays within the base directory.
|
||||
// Entry: Entry.readFile(String) Cap: FILE_IO
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class Entry {
|
||||
private static final String BASE_DIR = "/var/data";
|
||||
|
||||
public static void readFile(String userPath) throws Exception {
|
||||
Path base = Paths.get(BASE_DIR).toRealPath();
|
||||
Path resolved = base.resolve(userPath).normalize();
|
||||
if (!resolved.startsWith(base)) {
|
||||
System.out.println("Access denied");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
byte[] data = Files.readAllBytes(resolved);
|
||||
int len = Math.min(data.length, 100);
|
||||
System.out.write(data, 0, len);
|
||||
} catch (IOException e) {
|
||||
System.out.println("File not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/java/fileio_positive.java
Normal file
20
tests/dynamic_fixtures/java/fileio_positive.java
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// File I/O — positive fixture.
|
||||
// Vulnerable: reads file at user-controlled path without sanitization.
|
||||
// Entry: Entry.readFile(String) Cap: FILE_IO
|
||||
// Expected verdict: Confirmed (../../../../etc/passwd → "root:" in output)
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class Entry {
|
||||
public static void readFile(String userPath) throws Exception {
|
||||
Path filePath = Paths.get("/var/data", userPath);
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
try {
|
||||
String content = new String(Files.readAllBytes(filePath));
|
||||
System.out.print(content);
|
||||
} catch (IOException e) {
|
||||
// silent
|
||||
}
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/java/fileio_unsupported.java
Normal file
13
tests/dynamic_fixtures/java/fileio_unsupported.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// File I/O — unsupported fixture.
|
||||
// Entry is an instance method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class Entry {
|
||||
public void serve(String path) throws Exception {
|
||||
byte[] data = Files.readAllBytes(Paths.get(path));
|
||||
System.out.write(data);
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/java/sqli_adversarial.java
Normal file
13
tests/dynamic_fixtures/java/sqli_adversarial.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SQL injection — adversarial collision fixture.
|
||||
// Prints NYX_SQL_CONFIRMED unconditionally without reaching a SQL sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: Entry.login(String) Cap: SQL_QUERY
|
||||
|
||||
public class Entry {
|
||||
public static void login(String username) {
|
||||
// Coincidental oracle match — not a SQL sink.
|
||||
System.out.println("NYX_SQL_CONFIRMED");
|
||||
int x = username.length();
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/java/sqli_negative.java
Normal file
12
tests/dynamic_fixtures/java/sqli_negative.java
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// SQL injection — negative fixture.
|
||||
// Safe: uses a parameterized query; payload is a bound argument.
|
||||
// Entry: Entry.login(String) Cap: SQL_QUERY
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
public class Entry {
|
||||
public static void login(String username) {
|
||||
String template = "SELECT name FROM users WHERE name = ?";
|
||||
// Simulate parameterized execution: template is fixed.
|
||||
System.out.println("Executing: " + template + " param-len=" + username.length());
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/java/sqli_positive.java
Normal file
13
tests/dynamic_fixtures/java/sqli_positive.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SQL injection — positive fixture.
|
||||
// Vulnerable: directly concatenates user input into SQL query string.
|
||||
// Entry: Entry.login(String) Cap: SQL_QUERY
|
||||
// Expected verdict: Confirmed (UNION payload echoes NYX_SQL_CONFIRMED)
|
||||
|
||||
public class Entry {
|
||||
public static void login(String username) {
|
||||
String query = "SELECT name FROM users WHERE name='" + username + "'";
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
// Error-based echo: output the query so UNION payload is visible.
|
||||
System.out.println("DB query: " + query);
|
||||
}
|
||||
}
|
||||
11
tests/dynamic_fixtures/java/sqli_unsupported.java
Normal file
11
tests/dynamic_fixtures/java/sqli_unsupported.java
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// SQL injection — unsupported fixture.
|
||||
// Entry is an instance method rather than a static method.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
public class Entry {
|
||||
public void findUser(String name) {
|
||||
String query = "SELECT * FROM users WHERE name='" + name + "'";
|
||||
System.out.println(query);
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/java/ssrf_adversarial.java
Normal file
13
tests/dynamic_fixtures/java/ssrf_adversarial.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SSRF — adversarial collision fixture.
|
||||
// Prints "daemon:" unconditionally without making any HTTP request
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: Entry.fetchUrl(String) Cap: SSRF
|
||||
|
||||
public class Entry {
|
||||
public static void fetchUrl(String targetUrl) {
|
||||
// Coincidental oracle match — not an HTTP sink.
|
||||
System.out.println("daemon: present");
|
||||
int x = targetUrl.length();
|
||||
}
|
||||
}
|
||||
27
tests/dynamic_fixtures/java/ssrf_negative.java
Normal file
27
tests/dynamic_fixtures/java/ssrf_negative.java
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// SSRF — negative fixture.
|
||||
// Safe: only allows http/https scheme; file:// rejected.
|
||||
// Entry: Entry.fetchUrl(String) Cap: SSRF
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
public class Entry {
|
||||
public static void fetchUrl(String targetUrl) throws Exception {
|
||||
URL url = new URL(targetUrl);
|
||||
String proto = url.getProtocol();
|
||||
if (!proto.equals("http") && !proto.equals("https")) {
|
||||
System.out.println("Scheme not allowed: " + proto);
|
||||
return;
|
||||
}
|
||||
try (InputStream in = url.openStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line.substring(0, Math.min(line.length(), 64)));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println("Connection error");
|
||||
}
|
||||
}
|
||||
}
|
||||
24
tests/dynamic_fixtures/java/ssrf_positive.java
Normal file
24
tests/dynamic_fixtures/java/ssrf_positive.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// SSRF — positive fixture.
|
||||
// Vulnerable: makes a request to a user-controlled URL.
|
||||
// Entry: Entry.fetchUrl(String) Cap: SSRF
|
||||
// Expected verdict: Confirmed (file:///etc/passwd → "daemon:" in output)
|
||||
// Note: Java URL supports file:// scheme natively.
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
public class Entry {
|
||||
public static void fetchUrl(String targetUrl) throws Exception {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
URL url = new URL(targetUrl);
|
||||
try (InputStream in = url.openStream();
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// silent
|
||||
}
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/java/ssrf_unsupported.java
Normal file
12
tests/dynamic_fixtures/java/ssrf_unsupported.java
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// SSRF — unsupported fixture.
|
||||
// Entry is an instance method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
import java.io.*;
|
||||
import java.net.*;
|
||||
|
||||
public class Entry {
|
||||
public void fetch(String url) throws Exception {
|
||||
new URL(url).openStream().close();
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/java/xss_adversarial.java
Normal file
13
tests/dynamic_fixtures/java/xss_adversarial.java
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// XSS — adversarial collision fixture.
|
||||
// Prints the XSS oracle marker unconditionally without rendering any template
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: Entry.renderPage(String) Cap: HTML_ESCAPE
|
||||
|
||||
public class Entry {
|
||||
public static void renderPage(String userInput) {
|
||||
// Coincidental oracle match — not an HTML render sink.
|
||||
System.out.println("<script>NYX_XSS_CONFIRMED</script>");
|
||||
int x = userInput.length();
|
||||
}
|
||||
}
|
||||
19
tests/dynamic_fixtures/java/xss_negative.java
Normal file
19
tests/dynamic_fixtures/java/xss_negative.java
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// XSS — negative fixture.
|
||||
// Safe: HTML-encodes special characters before output.
|
||||
// Entry: Entry.renderPage(String) Cap: HTML_ESCAPE
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
public class Entry {
|
||||
private static String escapeHtml(String s) {
|
||||
return s.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
public static void renderPage(String userInput) {
|
||||
String safe = escapeHtml(userInput);
|
||||
System.out.print("<html><body>" + safe + "</body></html>\n");
|
||||
}
|
||||
}
|
||||
11
tests/dynamic_fixtures/java/xss_positive.java
Normal file
11
tests/dynamic_fixtures/java/xss_positive.java
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// XSS — positive fixture.
|
||||
// Vulnerable: echoes raw user input into HTML output without escaping.
|
||||
// Entry: Entry.renderPage(String) Cap: HTML_ESCAPE
|
||||
// Expected verdict: Confirmed (<script>NYX_XSS_CONFIRMED</script> echoed)
|
||||
|
||||
public class Entry {
|
||||
public static void renderPage(String userInput) {
|
||||
System.out.print("__NYX_SINK_HIT__\n");
|
||||
System.out.print("<html><body>" + userInput + "</body></html>\n");
|
||||
}
|
||||
}
|
||||
9
tests/dynamic_fixtures/java/xss_unsupported.java
Normal file
9
tests/dynamic_fixtures/java/xss_unsupported.java
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
// XSS — unsupported fixture.
|
||||
// Entry is an instance method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
public class Entry {
|
||||
public void render(String input) {
|
||||
System.out.print("<html><body>" + input + "</body></html>\n");
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/js/cmdi_adversarial.js
Normal file
13
tests/dynamic_fixtures/js/cmdi_adversarial.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Command injection — adversarial collision fixture.
|
||||
// Prints NYX_PWN_CMDI unconditionally without reaching a command sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: runPing(host) Cap: CODE_EXEC
|
||||
|
||||
function runPing(host) {
|
||||
// Coincidental oracle match — not a shell sink.
|
||||
process.stdout.write('NYX_PWN_CMDI\n');
|
||||
void host.length;
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
18
tests/dynamic_fixtures/js/cmdi_negative.js
Normal file
18
tests/dynamic_fixtures/js/cmdi_negative.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Command injection — negative fixture.
|
||||
// Safe: uses execFile (no shell) with args array; shell metacharacters ignored.
|
||||
// Entry: runPing(host) Cap: CODE_EXEC
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
function runPing(host) {
|
||||
// execFile does not invoke a shell — semicolons and metacharacters are inert.
|
||||
try {
|
||||
const out = execFileSync('echo', ['hello', host], { encoding: 'utf8', timeout: 5000 });
|
||||
process.stdout.write(out);
|
||||
} catch (e) {
|
||||
process.stdout.write('error\n');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
18
tests/dynamic_fixtures/js/cmdi_positive.js
Normal file
18
tests/dynamic_fixtures/js/cmdi_positive.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Command injection — positive fixture.
|
||||
// Vulnerable: passes user input directly to shell via execSync.
|
||||
// Entry: runPing(host) Cap: CODE_EXEC
|
||||
// Expected verdict: Confirmed ("; echo NYX_PWN_CMDI" payload echoes marker)
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function runPing(host) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const out = execSync('echo hello ' + host, { encoding: 'utf8', timeout: 5000 });
|
||||
process.stdout.write(out);
|
||||
} catch (e) {
|
||||
process.stdout.write((e.stdout || '') + (e.stderr || ''));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { runPing };
|
||||
17
tests/dynamic_fixtures/js/cmdi_unsupported.js
Normal file
17
tests/dynamic_fixtures/js/cmdi_unsupported.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Command injection — unsupported fixture.
|
||||
// Entry expects a pre-parsed args array, not a string payload.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Entry: runCommand(args) Cap: CODE_EXEC
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
function runCommand(args) {
|
||||
// args is expected to be an array; a string payload can't be routed here.
|
||||
if (!Array.isArray(args) || args.length === 0) {
|
||||
return;
|
||||
}
|
||||
execFileSync(args[0], args.slice(1), { encoding: 'utf8', timeout: 5000 });
|
||||
}
|
||||
|
||||
module.exports = { runCommand };
|
||||
13
tests/dynamic_fixtures/js/fileio_adversarial.js
Normal file
13
tests/dynamic_fixtures/js/fileio_adversarial.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// File I/O — adversarial collision fixture.
|
||||
// Prints "root:" unconditionally without reading any file
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: readFile(userPath) Cap: FILE_IO
|
||||
|
||||
function readFile(userPath) {
|
||||
// Coincidental oracle match — not a file read sink.
|
||||
process.stdout.write('root: present\n');
|
||||
void userPath.length;
|
||||
}
|
||||
|
||||
module.exports = { readFile };
|
||||
25
tests/dynamic_fixtures/js/fileio_negative.js
Normal file
25
tests/dynamic_fixtures/js/fileio_negative.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// File I/O — negative fixture.
|
||||
// Safe: path is normalized and validated against an allowlist prefix.
|
||||
// Entry: readFile(userPath) Cap: FILE_IO
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BASE_DIR = '/var/data';
|
||||
|
||||
function readFile(userPath) {
|
||||
const resolved = path.resolve(BASE_DIR, userPath);
|
||||
if (!resolved.startsWith(BASE_DIR + path.sep) && resolved !== BASE_DIR) {
|
||||
process.stdout.write('Access denied\n');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const content = fs.readFileSync(resolved, 'utf8');
|
||||
process.stdout.write(content.substring(0, 100));
|
||||
} catch (e) {
|
||||
process.stdout.write('File not found\n');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { readFile };
|
||||
20
tests/dynamic_fixtures/js/fileio_positive.js
Normal file
20
tests/dynamic_fixtures/js/fileio_positive.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// File I/O — positive fixture.
|
||||
// Vulnerable: reads a file at a user-controlled path without sanitization.
|
||||
// Entry: readFile(userPath) Cap: FILE_IO
|
||||
// Expected verdict: Confirmed (../../../../etc/passwd → "root:" in output)
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function readFile(userPath) {
|
||||
const filePath = path.join('/var/data', userPath);
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
process.stdout.write(content);
|
||||
} catch (e) {
|
||||
// Silent on error — target file may not exist in all envs
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { readFile };
|
||||
20
tests/dynamic_fixtures/js/fileio_unsupported.js
Normal file
20
tests/dynamic_fixtures/js/fileio_unsupported.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// File I/O — unsupported fixture.
|
||||
// Entry takes a Buffer (binary), not a UTF-8 string payload.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Entry: processUpload(buf) Cap: FILE_IO
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
function processUpload(buf) {
|
||||
if (!Buffer.isBuffer(buf)) {
|
||||
return;
|
||||
}
|
||||
const tmpPath = '/tmp/upload_' + Date.now();
|
||||
fs.writeFileSync(tmpPath, buf);
|
||||
const content = fs.readFileSync(tmpPath, 'utf8');
|
||||
process.stdout.write(content.substring(0, 64));
|
||||
fs.unlinkSync(tmpPath);
|
||||
}
|
||||
|
||||
module.exports = { processUpload };
|
||||
14
tests/dynamic_fixtures/js/sqli_adversarial.js
Normal file
14
tests/dynamic_fixtures/js/sqli_adversarial.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// SQL injection — adversarial collision fixture.
|
||||
// Prints NYX_SQL_CONFIRMED unconditionally without reaching a SQL sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// oracle_fired = true, sink_hit = false
|
||||
// Entry: login(username) Cap: SQL_QUERY
|
||||
|
||||
function login(username) {
|
||||
// Coincidental oracle match — not a SQL sink.
|
||||
process.stdout.write('NYX_SQL_CONFIRMED\n');
|
||||
void username.length;
|
||||
}
|
||||
|
||||
module.exports = { login };
|
||||
14
tests/dynamic_fixtures/js/sqli_negative.js
Normal file
14
tests/dynamic_fixtures/js/sqli_negative.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// SQL injection — negative fixture.
|
||||
// Safe: uses a parameterized query pattern; payload never concatenated.
|
||||
// Entry: login(username) Cap: SQL_QUERY
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
function login(username) {
|
||||
// Parameterized: the query template is fixed, payload is a bound param.
|
||||
const template = 'SELECT name FROM users WHERE name = ?';
|
||||
// Simulate param binding — payload is never embedded in the query string.
|
||||
const safeQuery = template; // template unchanged regardless of username
|
||||
process.stdout.write('Query executed with param: ' + safeQuery + '\n');
|
||||
}
|
||||
|
||||
module.exports = { login };
|
||||
13
tests/dynamic_fixtures/js/sqli_positive.js
Normal file
13
tests/dynamic_fixtures/js/sqli_positive.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SQL injection — positive fixture.
|
||||
// Vulnerable: directly concatenates user input into SQL query string.
|
||||
// Entry: login(username) Cap: SQL_QUERY
|
||||
// Expected verdict: Confirmed (UNION payload echoes NYX_SQL_CONFIRMED)
|
||||
|
||||
function login(username) {
|
||||
const query = "SELECT name FROM users WHERE name='" + username + "'";
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
// Simulate error-based SQL execution: echo query on failure.
|
||||
process.stdout.write('DB query: ' + query + '\n');
|
||||
}
|
||||
|
||||
module.exports = { login };
|
||||
15
tests/dynamic_fixtures/js/sqli_unsupported.js
Normal file
15
tests/dynamic_fixtures/js/sqli_unsupported.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// SQL injection — unsupported fixture.
|
||||
// The entry point accepts an HTTP request object, not a plain string.
|
||||
// HarnessSpec derivation will map this to HttpBody slot (unsupported by JS emitter).
|
||||
// Entry: handleRequest(req) Cap: SQL_QUERY
|
||||
// Expected verdict: Unsupported (EntryKindUnsupported or ConfidenceTooLow)
|
||||
|
||||
class UserRepository {
|
||||
findUser(req) {
|
||||
const name = req && req.body && req.body.name;
|
||||
const query = "SELECT * FROM users WHERE name='" + name + "'";
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { UserRepository };
|
||||
13
tests/dynamic_fixtures/js/ssrf_adversarial.js
Normal file
13
tests/dynamic_fixtures/js/ssrf_adversarial.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// SSRF — adversarial collision fixture.
|
||||
// Prints "daemon:" unconditionally without making any HTTP request
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: fetchUrl(targetUrl) Cap: SSRF
|
||||
|
||||
function fetchUrl(targetUrl) {
|
||||
// Coincidental oracle match — not an HTTP sink.
|
||||
process.stdout.write('daemon: present\n');
|
||||
void targetUrl.length;
|
||||
}
|
||||
|
||||
module.exports = { fetchUrl };
|
||||
24
tests/dynamic_fixtures/js/ssrf_negative.js
Normal file
24
tests/dynamic_fixtures/js/ssrf_negative.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
// SSRF — negative fixture.
|
||||
// Safe: only allows http/https scheme; file:// and other schemes are rejected.
|
||||
// Entry: fetchUrl(targetUrl) Cap: SSRF
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const url = require('url');
|
||||
|
||||
function fetchUrl(targetUrl) {
|
||||
const parsed = url.parse(targetUrl);
|
||||
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
||||
process.stdout.write('Scheme not allowed: ' + parsed.protocol + '\n');
|
||||
return;
|
||||
}
|
||||
const mod = parsed.protocol === 'https:' ? https : http;
|
||||
const req = mod.get(targetUrl, (res) => {
|
||||
res.on('data', (chunk) => process.stdout.write(chunk.toString().substring(0, 64)));
|
||||
});
|
||||
req.on('error', () => {});
|
||||
req.end();
|
||||
}
|
||||
|
||||
module.exports = { fetchUrl };
|
||||
35
tests/dynamic_fixtures/js/ssrf_positive.js
Normal file
35
tests/dynamic_fixtures/js/ssrf_positive.js
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// SSRF — positive fixture.
|
||||
// Vulnerable: makes a request to a user-controlled URL.
|
||||
// Entry: fetch(url) Cap: SSRF
|
||||
// Expected verdict: Confirmed (file:///etc/passwd → "daemon:" in output)
|
||||
// Note: Node.js http/https module does not support file:// scheme.
|
||||
// We detect the file:// prefix and use fs.readFile directly to simulate
|
||||
// the SSRF behaviour (same oracle: reads /etc/passwd, outputs "daemon:").
|
||||
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const url = require('url');
|
||||
|
||||
function fetchUrl(targetUrl) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
const parsed = url.parse(targetUrl);
|
||||
if (parsed.protocol === 'file:') {
|
||||
// Simulate SSRF via file:// — read local file (oracle expects "daemon:")
|
||||
try {
|
||||
const content = fs.readFileSync(parsed.pathname || '/', 'utf8');
|
||||
process.stdout.write(content);
|
||||
} catch (e) {
|
||||
// silent
|
||||
}
|
||||
return;
|
||||
}
|
||||
const mod = parsed.protocol === 'https:' ? https : http;
|
||||
const req = mod.get(targetUrl, (res) => {
|
||||
res.on('data', (chunk) => process.stdout.write(chunk));
|
||||
});
|
||||
req.on('error', () => {});
|
||||
req.end();
|
||||
}
|
||||
|
||||
module.exports = { fetchUrl };
|
||||
20
tests/dynamic_fixtures/js/ssrf_unsupported.js
Normal file
20
tests/dynamic_fixtures/js/ssrf_unsupported.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// SSRF — unsupported fixture.
|
||||
// Entry takes an already-parsed URL object, not a string.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Entry: fetchParsed(parsedUrl) Cap: SSRF
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
const http = require('http');
|
||||
|
||||
function fetchParsed(parsedUrl) {
|
||||
if (!parsedUrl || typeof parsedUrl !== 'object') {
|
||||
return;
|
||||
}
|
||||
const req = http.get(parsedUrl, (res) => {
|
||||
res.on('data', (d) => process.stdout.write(d));
|
||||
});
|
||||
req.on('error', () => {});
|
||||
req.end();
|
||||
}
|
||||
|
||||
module.exports = { fetchParsed };
|
||||
13
tests/dynamic_fixtures/js/xss_adversarial.js
Normal file
13
tests/dynamic_fixtures/js/xss_adversarial.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// XSS — adversarial collision fixture.
|
||||
// Prints the XSS oracle marker unconditionally without rendering any template
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: renderPage(userInput) Cap: HTML_ESCAPE
|
||||
|
||||
function renderPage(userInput) {
|
||||
// Coincidental oracle match — not an HTML render sink.
|
||||
process.stdout.write('<script>NYX_XSS_CONFIRMED</script>\n');
|
||||
void userInput.length;
|
||||
}
|
||||
|
||||
module.exports = { renderPage };
|
||||
20
tests/dynamic_fixtures/js/xss_negative.js
Normal file
20
tests/dynamic_fixtures/js/xss_negative.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// XSS — negative fixture.
|
||||
// Safe: HTML-escapes all special characters before output.
|
||||
// Entry: renderPage(userInput) Cap: HTML_ESCAPE
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function renderPage(userInput) {
|
||||
const safe = escapeHtml(userInput);
|
||||
process.stdout.write('<html><body>' + safe + '</body></html>\n');
|
||||
}
|
||||
|
||||
module.exports = { renderPage };
|
||||
12
tests/dynamic_fixtures/js/xss_positive.js
Normal file
12
tests/dynamic_fixtures/js/xss_positive.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// XSS — positive fixture.
|
||||
// Vulnerable: echoes raw user input into HTML output without escaping.
|
||||
// Entry: renderPage(userInput) Cap: HTML_ESCAPE
|
||||
// Expected verdict: Confirmed (<script>NYX_XSS_CONFIRMED</script> echoed)
|
||||
|
||||
function renderPage(userInput) {
|
||||
process.stdout.write('__NYX_SINK_HIT__\n');
|
||||
// Unescaped output — script tags pass through verbatim.
|
||||
process.stdout.write('<html><body>' + userInput + '</body></html>\n');
|
||||
}
|
||||
|
||||
module.exports = { renderPage };
|
||||
13
tests/dynamic_fixtures/js/xss_unsupported.js
Normal file
13
tests/dynamic_fixtures/js/xss_unsupported.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// XSS — unsupported fixture.
|
||||
// Entry is a class method rather than a top-level function.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Entry: TemplateEngine.render(input) Cap: HTML_ESCAPE
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
class TemplateEngine {
|
||||
render(input) {
|
||||
return '<html><body>' + input + '</body></html>';
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { TemplateEngine };
|
||||
12
tests/dynamic_fixtures/php/cmdi_adversarial.php
Normal file
12
tests/dynamic_fixtures/php/cmdi_adversarial.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// Command injection — adversarial collision fixture.
|
||||
// Prints NYX_PWN_CMDI unconditionally without reaching a command sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: runPing($host) Cap: CODE_EXEC
|
||||
|
||||
function runPing($host) {
|
||||
// Coincidental oracle match — not a shell sink.
|
||||
echo "NYX_PWN_CMDI\n";
|
||||
$x = strlen($host);
|
||||
}
|
||||
14
tests/dynamic_fixtures/php/cmdi_negative.php
Normal file
14
tests/dynamic_fixtures/php/cmdi_negative.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
// Command injection — negative fixture.
|
||||
// Safe: uses escapeshellarg() to prevent shell injection.
|
||||
// Entry: runPing($host) Cap: CODE_EXEC
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
function runPing($host) {
|
||||
// escapeshellarg wraps in single quotes and escapes internal quotes.
|
||||
$safe = escapeshellarg($host);
|
||||
$output = shell_exec('echo hello ' . $safe);
|
||||
if ($output !== null) {
|
||||
echo $output;
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/php/cmdi_positive.php
Normal file
13
tests/dynamic_fixtures/php/cmdi_positive.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
// Command injection — positive fixture.
|
||||
// Vulnerable: passes user input directly to shell_exec.
|
||||
// Entry: runPing($host) Cap: CODE_EXEC
|
||||
// Expected verdict: Confirmed ("; echo NYX_PWN_CMDI" echoes the marker)
|
||||
|
||||
function runPing($host) {
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$output = shell_exec('echo hello ' . $host);
|
||||
if ($output !== null) {
|
||||
echo $output;
|
||||
}
|
||||
}
|
||||
10
tests/dynamic_fixtures/php/cmdi_unsupported.php
Normal file
10
tests/dynamic_fixtures/php/cmdi_unsupported.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// Command injection — unsupported fixture.
|
||||
// Entry is a class method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
class Runner {
|
||||
public function execute($cmd) {
|
||||
shell_exec($cmd);
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/php/fileio_adversarial.php
Normal file
12
tests/dynamic_fixtures/php/fileio_adversarial.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// File I/O — adversarial collision fixture.
|
||||
// Prints "root:" unconditionally without reading any file
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: readFile($userPath) Cap: FILE_IO
|
||||
|
||||
function readFile($userPath) {
|
||||
// Coincidental oracle match — not a file read sink.
|
||||
echo "root: present\n";
|
||||
$x = strlen($userPath);
|
||||
}
|
||||
20
tests/dynamic_fixtures/php/fileio_negative.php
Normal file
20
tests/dynamic_fixtures/php/fileio_negative.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
// File I/O — negative fixture.
|
||||
// Safe: realpath + prefix validation prevents directory traversal.
|
||||
// Entry: readFile($userPath) Cap: FILE_IO
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
function readFile($userPath) {
|
||||
$baseDir = '/var/data';
|
||||
$filePath = realpath($baseDir . '/' . $userPath);
|
||||
if ($filePath === false || strpos($filePath, $baseDir . DIRECTORY_SEPARATOR) !== 0) {
|
||||
echo "Access denied\n";
|
||||
return;
|
||||
}
|
||||
$content = @file_get_contents($filePath);
|
||||
if ($content !== false) {
|
||||
echo substr($content, 0, 100);
|
||||
} else {
|
||||
echo "File not found\n";
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/php/fileio_positive.php
Normal file
14
tests/dynamic_fixtures/php/fileio_positive.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
// File I/O — positive fixture.
|
||||
// Vulnerable: reads file at user-controlled path without sanitization.
|
||||
// Entry: readFile($userPath) Cap: FILE_IO
|
||||
// Expected verdict: Confirmed (../../../../etc/passwd → "root:" in output)
|
||||
|
||||
function readFile($userPath) {
|
||||
$filePath = '/var/data/' . $userPath;
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$content = @file_get_contents($filePath);
|
||||
if ($content !== false) {
|
||||
echo $content;
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/php/fileio_unsupported.php
Normal file
13
tests/dynamic_fixtures/php/fileio_unsupported.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
// File I/O — unsupported fixture.
|
||||
// Entry is a class method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
class FileServer {
|
||||
public function serve($path) {
|
||||
$content = @file_get_contents($path);
|
||||
if ($content !== false) {
|
||||
echo $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/php/sqli_adversarial.php
Normal file
12
tests/dynamic_fixtures/php/sqli_adversarial.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// SQL injection — adversarial collision fixture.
|
||||
// Prints NYX_SQL_CONFIRMED unconditionally without reaching a SQL sink
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: login($username) Cap: SQL_QUERY
|
||||
|
||||
function login($username) {
|
||||
// Coincidental oracle match — not a SQL sink.
|
||||
echo "NYX_SQL_CONFIRMED\n";
|
||||
$x = strlen($username);
|
||||
}
|
||||
11
tests/dynamic_fixtures/php/sqli_negative.php
Normal file
11
tests/dynamic_fixtures/php/sqli_negative.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
// SQL injection — negative fixture.
|
||||
// Safe: uses PDO prepared statement; payload is a bound param, not concatenated.
|
||||
// Entry: login($username) Cap: SQL_QUERY
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
function login($username) {
|
||||
$template = "SELECT name FROM users WHERE name = ?";
|
||||
// Simulate parameterized execution: template is fixed.
|
||||
echo "Executing: " . $template . " param-len=" . strlen($username) . "\n";
|
||||
}
|
||||
12
tests/dynamic_fixtures/php/sqli_positive.php
Normal file
12
tests/dynamic_fixtures/php/sqli_positive.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// SQL injection — positive fixture.
|
||||
// Vulnerable: directly concatenates user input into SQL query string.
|
||||
// Entry: login($username) Cap: SQL_QUERY
|
||||
// Expected verdict: Confirmed (UNION payload echoes NYX_SQL_CONFIRMED)
|
||||
|
||||
function login($username) {
|
||||
$query = "SELECT name FROM users WHERE name='" . $username . "'";
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
// Error-based echo: output the query so UNION payload is visible.
|
||||
echo "DB query: " . $query . "\n";
|
||||
}
|
||||
12
tests/dynamic_fixtures/php/sqli_unsupported.php
Normal file
12
tests/dynamic_fixtures/php/sqli_unsupported.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// SQL injection — unsupported fixture.
|
||||
// Entry is a class method — entry kind unsupported.
|
||||
// Test sets confidence = Low to get Unsupported(ConfidenceTooLow).
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
class UserRepository {
|
||||
public function findUser($name) {
|
||||
$query = "SELECT * FROM users WHERE name='" . $name . "'";
|
||||
echo $query . "\n";
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/php/ssrf_adversarial.php
Normal file
12
tests/dynamic_fixtures/php/ssrf_adversarial.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// SSRF — adversarial collision fixture.
|
||||
// Prints "daemon:" unconditionally without making any HTTP request
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: fetchUrl($url) Cap: SSRF
|
||||
|
||||
function fetchUrl($url) {
|
||||
// Coincidental oracle match — not an HTTP sink.
|
||||
echo "daemon: present\n";
|
||||
$x = strlen($url);
|
||||
}
|
||||
18
tests/dynamic_fixtures/php/ssrf_negative.php
Normal file
18
tests/dynamic_fixtures/php/ssrf_negative.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
// SSRF — negative fixture.
|
||||
// Safe: only allows http/https scheme; file:// and others rejected.
|
||||
// Entry: fetchUrl($url) Cap: SSRF
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
function fetchUrl($url) {
|
||||
$parsed = parse_url($url);
|
||||
$scheme = $parsed['scheme'] ?? '';
|
||||
if ($scheme !== 'http' && $scheme !== 'https') {
|
||||
echo "Scheme not allowed: " . $scheme . "\n";
|
||||
return;
|
||||
}
|
||||
$content = @file_get_contents($url);
|
||||
if ($content !== false) {
|
||||
echo substr($content, 0, 64);
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/php/ssrf_positive.php
Normal file
14
tests/dynamic_fixtures/php/ssrf_positive.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
// SSRF — positive fixture.
|
||||
// Vulnerable: fetches a user-controlled URL via file_get_contents.
|
||||
// PHP's file_get_contents supports file:// scheme natively.
|
||||
// Entry: fetchUrl($url) Cap: SSRF
|
||||
// Expected verdict: Confirmed (file:///etc/passwd → "daemon:" in output)
|
||||
|
||||
function fetchUrl($url) {
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
$content = @file_get_contents($url);
|
||||
if ($content !== false) {
|
||||
echo $content;
|
||||
}
|
||||
}
|
||||
13
tests/dynamic_fixtures/php/ssrf_unsupported.php
Normal file
13
tests/dynamic_fixtures/php/ssrf_unsupported.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
// SSRF — unsupported fixture.
|
||||
// Entry is a class method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
class HttpClient {
|
||||
public function fetch($url) {
|
||||
$content = @file_get_contents($url);
|
||||
if ($content !== false) {
|
||||
echo $content;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
tests/dynamic_fixtures/php/xss_adversarial.php
Normal file
12
tests/dynamic_fixtures/php/xss_adversarial.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
// XSS — adversarial collision fixture.
|
||||
// Prints the XSS oracle marker unconditionally without rendering any template
|
||||
// and without emitting __NYX_SINK_HIT__.
|
||||
// Expected verdict: Inconclusive(OracleCollisionSuspected)
|
||||
// Entry: renderPage($userInput) Cap: HTML_ESCAPE
|
||||
|
||||
function renderPage($userInput) {
|
||||
// Coincidental oracle match — not an HTML render sink.
|
||||
echo "<script>NYX_XSS_CONFIRMED</script>\n";
|
||||
$x = strlen($userInput);
|
||||
}
|
||||
10
tests/dynamic_fixtures/php/xss_negative.php
Normal file
10
tests/dynamic_fixtures/php/xss_negative.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// XSS — negative fixture.
|
||||
// Safe: uses htmlspecialchars() before output.
|
||||
// Entry: renderPage($userInput) Cap: HTML_ESCAPE
|
||||
// Expected verdict: NotConfirmed
|
||||
|
||||
function renderPage($userInput) {
|
||||
$safe = htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
|
||||
echo '<html><body>' . $safe . '</body></html>' . "\n";
|
||||
}
|
||||
10
tests/dynamic_fixtures/php/xss_positive.php
Normal file
10
tests/dynamic_fixtures/php/xss_positive.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// XSS — positive fixture.
|
||||
// Vulnerable: echoes raw user input into HTML output without escaping.
|
||||
// Entry: renderPage($userInput) Cap: HTML_ESCAPE
|
||||
// Expected verdict: Confirmed (<script>NYX_XSS_CONFIRMED</script> echoed)
|
||||
|
||||
function renderPage($userInput) {
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
echo '<html><body>' . $userInput . '</body></html>' . "\n";
|
||||
}
|
||||
10
tests/dynamic_fixtures/php/xss_unsupported.php
Normal file
10
tests/dynamic_fixtures/php/xss_unsupported.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
// XSS — unsupported fixture.
|
||||
// Entry is a class method; test sets confidence = Low.
|
||||
// Expected verdict: Unsupported
|
||||
|
||||
class TemplateEngine {
|
||||
public function render($input) {
|
||||
echo '<html><body>' . $input . '</body></html>' . "\n";
|
||||
}
|
||||
}
|
||||
|
|
@ -226,6 +226,112 @@ mod escape_tests {
|
|||
);
|
||||
}
|
||||
|
||||
// ── Build-step escape tests for Phase 05 languages ────────────────────────
|
||||
|
||||
/// Verify that a malicious npm lifecycle script (`preinstall`) cannot write
|
||||
/// to the host when `npm install` runs inside the sandbox.
|
||||
///
|
||||
/// NOTE (Phase 05): Docker + npm install is deferred. `prepare_node()` runs
|
||||
/// `npm install` via the process backend on the host — Docker isolation does
|
||||
/// NOT protect the build step yet.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/npm_malicious_lifecycle/package.json`
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated npm install not yet implemented"]
|
||||
fn escape_npm_malicious_lifecycle() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_npm_lifecycle");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated npm install and re-enable body.
|
||||
// When implemented: copy npm_malicious_lifecycle/ to temp workdir,
|
||||
// run prepare_node_in_docker(spec, workdir), assert !marker.exists().
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_npm_lifecycle must not exist before Docker+npm install is implemented"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that a malicious Go `init()` function cannot write to the host
|
||||
/// when the compiled binary runs inside the Docker sandbox.
|
||||
///
|
||||
/// NOTE (Phase 05): `go build` runs via the process backend on the host;
|
||||
/// the resulting binary executes inside Docker (sandboxed runtime). The
|
||||
/// `init()` write targets `/tmp/pwned_go_init` which is isolated inside
|
||||
/// the container — host marker must remain absent.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/go_malicious_init.go`
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated go build not yet implemented; init() runtime escape sandboxed by container /tmp isolation"]
|
||||
fn escape_go_malicious_init() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_go_init");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated go build, then run the binary
|
||||
// inside the sandbox and assert the host marker is absent.
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_go_init must not exist; Go init() write stays inside container /tmp"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that a malicious Maven plugin (`exec-maven-plugin`) cannot write
|
||||
/// to the host when `mvn compile` runs inside the sandbox.
|
||||
///
|
||||
/// NOTE (Phase 05): Docker + Maven compilation is deferred. `prepare_java()`
|
||||
/// runs `javac` via the process backend on the host — Docker isolation does
|
||||
/// NOT protect the build step yet.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/maven_malicious_plugin/pom.xml`
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated Maven build not yet implemented"]
|
||||
fn escape_maven_malicious_plugin() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_maven_plugin");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated mvn compile and re-enable body.
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_maven_plugin must not exist before Docker+Maven build is implemented"
|
||||
);
|
||||
}
|
||||
|
||||
/// Verify that a malicious Composer `post-install-cmd` cannot write to the
|
||||
/// host when `composer install` runs inside the sandbox.
|
||||
///
|
||||
/// NOTE (Phase 05): Docker + Composer install is deferred. `prepare_php()`
|
||||
/// runs `php` directly via the process backend — Docker isolation does NOT
|
||||
/// protect the install step yet.
|
||||
///
|
||||
/// Fixture: `tests/dynamic_fixtures/escape/composer_malicious_postinstall/composer.json`
|
||||
#[test]
|
||||
#[ignore = "Phase 06: Docker-isolated composer install not yet implemented"]
|
||||
fn escape_composer_malicious_postinstall() {
|
||||
if !docker_available() {
|
||||
return;
|
||||
}
|
||||
let marker = std::path::PathBuf::from("/tmp/pwned_composer_postinstall");
|
||||
let _ = fs::remove_file(&marker);
|
||||
|
||||
// Phase 06 TODO: wire Docker-isolated composer install and re-enable body.
|
||||
|
||||
assert!(
|
||||
!marker.exists(),
|
||||
"host marker /tmp/pwned_composer_postinstall must not exist before Docker+Composer install is implemented"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Positive control test ─────────────────────────────────────────────────
|
||||
|
||||
/// Positive control: verify the escape-detection mechanism itself.
|
||||
|
|
|
|||
|
|
@ -85,12 +85,46 @@ mod verify_e2e {
|
|||
}
|
||||
}
|
||||
|
||||
/// Same as `taint_diag_with_cap` but uses a C source file so that
|
||||
/// `HarnessSpec::from_finding` derives `Lang::C`, which has no emitter.
|
||||
fn taint_diag_c_lang(cap: Cap) -> Diag {
|
||||
Diag {
|
||||
path: "src/handler.c".into(),
|
||||
line: 10,
|
||||
col: 0,
|
||||
severity: Severity::High,
|
||||
id: "taint-unsanitised-flow".into(),
|
||||
category: FindingCategory::Security,
|
||||
path_validated: false,
|
||||
guard_kind: None,
|
||||
message: None,
|
||||
labels: vec![],
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(Evidence {
|
||||
flow_steps: vec![
|
||||
source_step("src/handler.c", "handle_request"),
|
||||
sink_step("src/handler.c"),
|
||||
],
|
||||
sink_caps: cap.bits(),
|
||||
..Default::default()
|
||||
}),
|
||||
rank_score: None,
|
||||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
stable_hash: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A finding with a supported cap (SQL_QUERY) and a derivable spec reaches
|
||||
/// `harness::build`. The finding uses a Rust entry file, so the Python-only
|
||||
/// harness emitter returns `LangUnsupported`.
|
||||
/// `harness::build`. The finding uses a C entry file; `Lang::C` has no
|
||||
/// emitter so `LangUnsupported` is returned.
|
||||
#[test]
|
||||
fn verify_finding_rust_lang_returns_lang_unsupported() {
|
||||
let diag = taint_diag_with_cap(Cap::SQL_QUERY);
|
||||
let diag = taint_diag_c_lang(Cap::SQL_QUERY);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
|
|
@ -127,12 +161,12 @@ mod verify_e2e {
|
|||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
/// The JSON shape of `VerifyResult` for a Rust finding (lang unsupported)
|
||||
/// The JSON shape of `VerifyResult` for a C finding (lang unsupported)
|
||||
/// matches the documented contract: `status`, `reason` present;
|
||||
/// `triggered_payload`, `detail`, `attempts` absent (skipped by serde).
|
||||
#[test]
|
||||
fn verify_result_json_shape_lang_unsupported() {
|
||||
let diag = taint_diag_with_cap(Cap::SQL_QUERY);
|
||||
let diag = taint_diag_c_lang(Cap::SQL_QUERY);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
|
|
|
|||
447
tests/go_fixtures.rs
Normal file
447
tests/go_fixtures.rs
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
//! Go fixture integration tests (Phase 05 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each Go fixture and asserts
|
||||
//! the expected verdict. Requires `--features dynamic` and `go` on PATH.
|
||||
//!
|
||||
//! Entry points follow: `func FuncName(payload string)` in package `entry`.
|
||||
//! The harness wraps each fixture in a generated `main.go` that reads
|
||||
//! `NYX_PAYLOAD` and calls `entry.FuncName(payload)`.
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test go_fixtures`
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod go_fixture_tests {
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
|
||||
use nyx_scanner::evidence::{
|
||||
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
|
||||
VerifyStatus,
|
||||
};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn go_available() -> bool {
|
||||
std::process::Command::new("go")
|
||||
.arg("version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/go")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn run_fixture(
|
||||
fixture: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
) -> nyx_scanner::evidence::VerifyResult {
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !go_available() {
|
||||
return nyx_scanner::evidence::VerifyResult {
|
||||
finding_id: String::new(),
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::BackendUnavailable),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
};
|
||||
}
|
||||
|
||||
let path = fixture_path(fixture);
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
|
||||
std::env::set_var(
|
||||
"NYX_TELEMETRY_PATH",
|
||||
tmp.path().join("events.jsonl").to_str().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let diag = make_diag(&path, func, cap, sink_line);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("NYX_REPRO_BASE");
|
||||
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ── SQLi fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn go_sqli_positive_is_confirmed() {
|
||||
let result = run_fixture("sqli_positive.go", "Login", Cap::SQL_QUERY, 13);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"sqli_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_sqli_negative_is_not_confirmed() {
|
||||
let result = run_fixture("sqli_negative.go", "Login", Cap::SQL_QUERY, 12);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"sqli_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_sqli_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("sqli_adversarial.go", "Login", Cap::SQL_QUERY, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_sqli_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("sqli_unsupported.go");
|
||||
let mut d = make_diag(&path, "FindUser", Cap::SQL_QUERY, 12);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Command injection fixtures ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn go_cmdi_positive_is_confirmed() {
|
||||
let result = run_fixture("cmdi_positive.go", "RunPing", Cap::CODE_EXEC, 15);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"cmdi_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_cmdi_negative_is_not_confirmed() {
|
||||
let result = run_fixture("cmdi_negative.go", "RunPing", Cap::CODE_EXEC, 14);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"cmdi_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_cmdi_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("cmdi_adversarial.go", "RunPing", Cap::CODE_EXEC, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_cmdi_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("cmdi_unsupported.go");
|
||||
let mut d = make_diag(&path, "Execute", Cap::CODE_EXEC, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── File I/O fixtures ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn go_fileio_positive_is_confirmed() {
|
||||
let result = run_fixture("fileio_positive.go", "ReadFile", Cap::FILE_IO, 17);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"fileio_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_fileio_negative_is_not_confirmed() {
|
||||
let result = run_fixture("fileio_negative.go", "ReadFile", Cap::FILE_IO, 20);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"fileio_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_fileio_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("fileio_adversarial.go", "ReadFile", Cap::FILE_IO, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_fileio_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("fileio_unsupported.go");
|
||||
let mut d = make_diag(&path, "Serve", Cap::FILE_IO, 13);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── SSRF fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn go_ssrf_positive_is_confirmed() {
|
||||
let result = run_fixture("ssrf_positive.go", "FetchURL", Cap::SSRF, 21);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"ssrf_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_ssrf_negative_is_not_confirmed() {
|
||||
let result = run_fixture("ssrf_negative.go", "FetchURL", Cap::SSRF, 18);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"ssrf_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_ssrf_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("ssrf_adversarial.go", "FetchURL", Cap::SSRF, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_ssrf_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("ssrf_unsupported.go");
|
||||
let mut d = make_diag(&path, "Fetch", Cap::SSRF, 11);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── XSS fixtures ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn go_xss_positive_is_confirmed() {
|
||||
let result = run_fixture("xss_positive.go", "RenderPage", Cap::HTML_ESCAPE, 12);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"xss_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_xss_negative_is_not_confirmed() {
|
||||
let result = run_fixture("xss_negative.go", "RenderPage", Cap::HTML_ESCAPE, 12);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"xss_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_xss_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("xss_adversarial.go", "RenderPage", Cap::HTML_ESCAPE, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_xss_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("xss_unsupported.go");
|
||||
let mut d = make_diag(&path, "Render", Cap::HTML_ESCAPE, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_diag(path: &Path, func: &str, cap: Cap, sink_line: u32) -> Diag {
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
let evidence = Evidence {
|
||||
flow_steps: vec![
|
||||
FlowStep {
|
||||
step: 1,
|
||||
kind: FlowStepKind::Source,
|
||||
file: path_str.clone(),
|
||||
line: 1,
|
||||
col: 0,
|
||||
snippet: None,
|
||||
variable: Some("payload".into()),
|
||||
callee: None,
|
||||
function: Some(func.to_owned()),
|
||||
is_cross_file: false,
|
||||
},
|
||||
FlowStep {
|
||||
step: 2,
|
||||
kind: FlowStepKind::Sink,
|
||||
file: path_str.clone(),
|
||||
line: sink_line,
|
||||
col: 4,
|
||||
snippet: None,
|
||||
variable: None,
|
||||
callee: None,
|
||||
function: None,
|
||||
is_cross_file: false,
|
||||
},
|
||||
],
|
||||
sink_caps: cap.bits(),
|
||||
..Default::default()
|
||||
};
|
||||
Diag {
|
||||
path: path_str,
|
||||
line: sink_line as usize,
|
||||
col: 0,
|
||||
severity: Severity::High,
|
||||
id: "taint-unsanitised-flow".into(),
|
||||
category: FindingCategory::Security,
|
||||
path_validated: false,
|
||||
guard_kind: None,
|
||||
message: None,
|
||||
labels: vec![],
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(evidence),
|
||||
rank_score: None,
|
||||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
stable_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
447
tests/java_fixtures.rs
Normal file
447
tests/java_fixtures.rs
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
//! Java fixture integration tests (Phase 05 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each Java fixture and asserts
|
||||
//! the expected verdict. Requires `--features dynamic` and `java`/`javac` on PATH.
|
||||
//!
|
||||
//! Entry points follow: `public static void FuncName(String)` in class `Entry`.
|
||||
//! The harness wraps each fixture in a generated `NyxHarness.java` that reads
|
||||
//! `NYX_PAYLOAD` and calls `Entry.FuncName(payload)`.
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test java_fixtures`
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod java_fixture_tests {
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
|
||||
use nyx_scanner::evidence::{
|
||||
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
|
||||
VerifyStatus,
|
||||
};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn java_available() -> bool {
|
||||
std::process::Command::new("java")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/java")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn run_fixture(
|
||||
fixture: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
) -> nyx_scanner::evidence::VerifyResult {
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !java_available() {
|
||||
return nyx_scanner::evidence::VerifyResult {
|
||||
finding_id: String::new(),
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::BackendUnavailable),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
};
|
||||
}
|
||||
|
||||
let path = fixture_path(fixture);
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
|
||||
std::env::set_var(
|
||||
"NYX_TELEMETRY_PATH",
|
||||
tmp.path().join("events.jsonl").to_str().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let diag = make_diag(&path, func, cap, sink_line);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("NYX_REPRO_BASE");
|
||||
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ── SQLi fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn java_sqli_positive_is_confirmed() {
|
||||
let result = run_fixture("sqli_positive.java", "login", Cap::SQL_QUERY, 9);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"sqli_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_sqli_negative_is_not_confirmed() {
|
||||
let result = run_fixture("sqli_negative.java", "login", Cap::SQL_QUERY, 10);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"sqli_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_sqli_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("sqli_adversarial.java", "login", Cap::SQL_QUERY, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_sqli_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("sqli_unsupported.java");
|
||||
let mut d = make_diag(&path, "findUser", Cap::SQL_QUERY, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Command injection fixtures ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn java_cmdi_positive_is_confirmed() {
|
||||
let result = run_fixture("cmdi_positive.java", "runPing", Cap::CODE_EXEC, 10);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"cmdi_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_cmdi_negative_is_not_confirmed() {
|
||||
let result = run_fixture("cmdi_negative.java", "runPing", Cap::CODE_EXEC, 12);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"cmdi_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_cmdi_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("cmdi_adversarial.java", "runPing", Cap::CODE_EXEC, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_cmdi_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("cmdi_unsupported.java");
|
||||
let mut d = make_diag(&path, "execute", Cap::CODE_EXEC, 9);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── File I/O fixtures ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn java_fileio_positive_is_confirmed() {
|
||||
let result = run_fixture("fileio_positive.java", "readFile", Cap::FILE_IO, 12);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"fileio_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_fileio_negative_is_not_confirmed() {
|
||||
let result = run_fixture("fileio_negative.java", "readFile", Cap::FILE_IO, 20);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"fileio_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_fileio_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("fileio_adversarial.java", "readFile", Cap::FILE_IO, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_fileio_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("fileio_unsupported.java");
|
||||
let mut d = make_diag(&path, "serve", Cap::FILE_IO, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── SSRF fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn java_ssrf_positive_is_confirmed() {
|
||||
let result = run_fixture("ssrf_positive.java", "fetchUrl", Cap::SSRF, 12);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"ssrf_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_ssrf_negative_is_not_confirmed() {
|
||||
let result = run_fixture("ssrf_negative.java", "fetchUrl", Cap::SSRF, 17);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"ssrf_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_ssrf_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("ssrf_adversarial.java", "fetchUrl", Cap::SSRF, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_ssrf_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("ssrf_unsupported.java");
|
||||
let mut d = make_diag(&path, "fetch", Cap::SSRF, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── XSS fixtures ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn java_xss_positive_is_confirmed() {
|
||||
let result = run_fixture("xss_positive.java", "renderPage", Cap::HTML_ESCAPE, 8);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"xss_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_xss_negative_is_not_confirmed() {
|
||||
let result = run_fixture("xss_negative.java", "renderPage", Cap::HTML_ESCAPE, 17);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"xss_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_xss_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("xss_adversarial.java", "renderPage", Cap::HTML_ESCAPE, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_xss_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("xss_unsupported.java");
|
||||
let mut d = make_diag(&path, "render", Cap::HTML_ESCAPE, 7);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_diag(path: &Path, func: &str, cap: Cap, sink_line: u32) -> Diag {
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
let evidence = Evidence {
|
||||
flow_steps: vec![
|
||||
FlowStep {
|
||||
step: 1,
|
||||
kind: FlowStepKind::Source,
|
||||
file: path_str.clone(),
|
||||
line: 1,
|
||||
col: 0,
|
||||
snippet: None,
|
||||
variable: Some("payload".into()),
|
||||
callee: None,
|
||||
function: Some(func.to_owned()),
|
||||
is_cross_file: false,
|
||||
},
|
||||
FlowStep {
|
||||
step: 2,
|
||||
kind: FlowStepKind::Sink,
|
||||
file: path_str.clone(),
|
||||
line: sink_line,
|
||||
col: 4,
|
||||
snippet: None,
|
||||
variable: None,
|
||||
callee: None,
|
||||
function: None,
|
||||
is_cross_file: false,
|
||||
},
|
||||
],
|
||||
sink_caps: cap.bits(),
|
||||
..Default::default()
|
||||
};
|
||||
Diag {
|
||||
path: path_str,
|
||||
line: sink_line as usize,
|
||||
col: 0,
|
||||
severity: Severity::High,
|
||||
id: "taint-unsanitised-flow".into(),
|
||||
category: FindingCategory::Security,
|
||||
path_validated: false,
|
||||
guard_kind: None,
|
||||
message: None,
|
||||
labels: vec![],
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(evidence),
|
||||
rank_score: None,
|
||||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
stable_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
452
tests/js_fixtures.rs
Normal file
452
tests/js_fixtures.rs
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
//! JavaScript/Node.js fixture integration tests (Phase 05 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each JS fixture and asserts
|
||||
//! the expected verdict. Requires `--features dynamic` and `node` on PATH.
|
||||
//!
|
||||
//! Entry points follow: `function funcName(payload)` + `module.exports = { funcName }`.
|
||||
//! The harness emitter wraps each fixture in a generated `harness.js` that
|
||||
//! reads `NYX_PAYLOAD` from the environment and calls `_entry.funcName(payload)`.
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test js_fixtures`
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod js_fixture_tests {
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
|
||||
use nyx_scanner::evidence::{
|
||||
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
|
||||
VerifyStatus,
|
||||
};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn node_available() -> bool {
|
||||
std::process::Command::new("node")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/js")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
/// Run a JS fixture through the full dynamic verification pipeline.
|
||||
///
|
||||
/// The fixture file is copied to a temp dir as `entry.js`.
|
||||
fn run_fixture(
|
||||
fixture: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
) -> nyx_scanner::evidence::VerifyResult {
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !node_available() {
|
||||
return nyx_scanner::evidence::VerifyResult {
|
||||
finding_id: String::new(),
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::BackendUnavailable),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
};
|
||||
}
|
||||
|
||||
let path = fixture_path(fixture);
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let dst = tmp.path().join("entry.js");
|
||||
std::fs::copy(&path, &dst).expect("fixture file must exist");
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
|
||||
std::env::set_var(
|
||||
"NYX_TELEMETRY_PATH",
|
||||
tmp.path().join("events.jsonl").to_str().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let diag = make_diag(&path, func, cap, sink_line);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("NYX_REPRO_BASE");
|
||||
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ── SQLi fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn js_sqli_positive_is_confirmed() {
|
||||
let result = run_fixture("sqli_positive.js", "login", Cap::SQL_QUERY, 12);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return; // node not available
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"sqli_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_sqli_negative_is_not_confirmed() {
|
||||
let result = run_fixture("sqli_negative.js", "login", Cap::SQL_QUERY, 13);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"sqli_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_sqli_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("sqli_adversarial.js", "login", Cap::SQL_QUERY, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_sqli_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("sqli_unsupported.js");
|
||||
let mut d = make_diag(&path, "findUser", Cap::SQL_QUERY, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Command injection fixtures ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn js_cmdi_positive_is_confirmed() {
|
||||
let result = run_fixture("cmdi_positive.js", "runPing", Cap::CODE_EXEC, 11);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"cmdi_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_cmdi_negative_is_not_confirmed() {
|
||||
let result = run_fixture("cmdi_negative.js", "runPing", Cap::CODE_EXEC, 11);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"cmdi_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_cmdi_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("cmdi_adversarial.js", "runPing", Cap::CODE_EXEC, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_cmdi_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("cmdi_unsupported.js");
|
||||
let mut d = make_diag(&path, "runCommand", Cap::CODE_EXEC, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── File I/O fixtures ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn js_fileio_positive_is_confirmed() {
|
||||
let result = run_fixture("fileio_positive.js", "readFile", Cap::FILE_IO, 13);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"fileio_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_fileio_negative_is_not_confirmed() {
|
||||
let result = run_fixture("fileio_negative.js", "readFile", Cap::FILE_IO, 16);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"fileio_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_fileio_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("fileio_adversarial.js", "readFile", Cap::FILE_IO, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_fileio_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("fileio_unsupported.js");
|
||||
let mut d = make_diag(&path, "processUpload", Cap::FILE_IO, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── SSRF fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn js_ssrf_positive_is_confirmed() {
|
||||
let result = run_fixture("ssrf_positive.js", "fetchUrl", Cap::SSRF, 21);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"ssrf_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_ssrf_negative_is_not_confirmed() {
|
||||
let result = run_fixture("ssrf_negative.js", "fetchUrl", Cap::SSRF, 16);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"ssrf_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_ssrf_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("ssrf_adversarial.js", "fetchUrl", Cap::SSRF, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_ssrf_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("ssrf_unsupported.js");
|
||||
let mut d = make_diag(&path, "fetchParsed", Cap::SSRF, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── XSS fixtures ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn js_xss_positive_is_confirmed() {
|
||||
let result = run_fixture("xss_positive.js", "renderPage", Cap::HTML_ESCAPE, 10);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"xss_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_xss_negative_is_not_confirmed() {
|
||||
let result = run_fixture("xss_negative.js", "renderPage", Cap::HTML_ESCAPE, 14);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"xss_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_xss_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("xss_adversarial.js", "renderPage", Cap::HTML_ESCAPE, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_xss_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("xss_unsupported.js");
|
||||
let mut d = make_diag(&path, "render", Cap::HTML_ESCAPE, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_diag(path: &Path, func: &str, cap: Cap, sink_line: u32) -> Diag {
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
let evidence = Evidence {
|
||||
flow_steps: vec![
|
||||
FlowStep {
|
||||
step: 1,
|
||||
kind: FlowStepKind::Source,
|
||||
file: path_str.clone(),
|
||||
line: 1,
|
||||
col: 0,
|
||||
snippet: None,
|
||||
variable: Some("payload".into()),
|
||||
callee: None,
|
||||
function: Some(func.to_owned()),
|
||||
is_cross_file: false,
|
||||
},
|
||||
FlowStep {
|
||||
step: 2,
|
||||
kind: FlowStepKind::Sink,
|
||||
file: path_str.clone(),
|
||||
line: sink_line,
|
||||
col: 4,
|
||||
snippet: None,
|
||||
variable: None,
|
||||
callee: None,
|
||||
function: None,
|
||||
is_cross_file: false,
|
||||
},
|
||||
],
|
||||
sink_caps: cap.bits(),
|
||||
..Default::default()
|
||||
};
|
||||
Diag {
|
||||
path: path_str,
|
||||
line: sink_line as usize,
|
||||
col: 0,
|
||||
severity: Severity::High,
|
||||
id: "taint-unsanitised-flow".into(),
|
||||
category: FindingCategory::Security,
|
||||
path_validated: false,
|
||||
guard_kind: None,
|
||||
message: None,
|
||||
labels: vec![],
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(evidence),
|
||||
rank_score: None,
|
||||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
stable_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
447
tests/php_fixtures.rs
Normal file
447
tests/php_fixtures.rs
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
//! PHP fixture integration tests (Phase 05 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each PHP fixture and asserts
|
||||
//! the expected verdict. Requires `--features dynamic` and `php` on PATH.
|
||||
//!
|
||||
//! Entry points follow: `function funcName($payload)` at top level.
|
||||
//! The harness wraps each fixture in a generated runner that reads
|
||||
//! `NYX_PAYLOAD` and calls `funcName($payload)`.
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test php_fixtures`
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod php_fixture_tests {
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
use nyx_scanner::dynamic::verify::{verify_finding, VerifyOptions};
|
||||
use nyx_scanner::evidence::{
|
||||
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
|
||||
VerifyStatus,
|
||||
};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static FIXTURE_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn php_available() -> bool {
|
||||
std::process::Command::new("php")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/dynamic_fixtures/php")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn run_fixture(
|
||||
fixture: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
) -> nyx_scanner::evidence::VerifyResult {
|
||||
let _guard = FIXTURE_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !php_available() {
|
||||
return nyx_scanner::evidence::VerifyResult {
|
||||
finding_id: String::new(),
|
||||
status: VerifyStatus::Unsupported,
|
||||
triggered_payload: None,
|
||||
reason: Some(UnsupportedReason::BackendUnavailable),
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
};
|
||||
}
|
||||
|
||||
let path = fixture_path(fixture);
|
||||
let tmp = TempDir::new().unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var("NYX_REPRO_BASE", tmp.path().join("repro").to_str().unwrap());
|
||||
std::env::set_var(
|
||||
"NYX_TELEMETRY_PATH",
|
||||
tmp.path().join("events.jsonl").to_str().unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let diag = make_diag(&path, func, cap, sink_line);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("NYX_REPRO_BASE");
|
||||
std::env::remove_var("NYX_TELEMETRY_PATH");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
// ── SQLi fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn php_sqli_positive_is_confirmed() {
|
||||
let result = run_fixture("sqli_positive.php", "login", Cap::SQL_QUERY, 9);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"sqli_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_sqli_negative_is_not_confirmed() {
|
||||
let result = run_fixture("sqli_negative.php", "login", Cap::SQL_QUERY, 10);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"sqli_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_sqli_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("sqli_adversarial.php", "login", Cap::SQL_QUERY, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_sqli_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("sqli_unsupported.php");
|
||||
let mut d = make_diag(&path, "findUser", Cap::SQL_QUERY, 10);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Command injection fixtures ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn php_cmdi_positive_is_confirmed() {
|
||||
let result = run_fixture("cmdi_positive.php", "runPing", Cap::CODE_EXEC, 8);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"cmdi_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_cmdi_negative_is_not_confirmed() {
|
||||
let result = run_fixture("cmdi_negative.php", "runPing", Cap::CODE_EXEC, 10);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"cmdi_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_cmdi_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("cmdi_adversarial.php", "runPing", Cap::CODE_EXEC, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_cmdi_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("cmdi_unsupported.php");
|
||||
let mut d = make_diag(&path, "execute", Cap::CODE_EXEC, 8);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── File I/O fixtures ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn php_fileio_positive_is_confirmed() {
|
||||
let result = run_fixture("fileio_positive.php", "readFile", Cap::FILE_IO, 9);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"fileio_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_fileio_negative_is_not_confirmed() {
|
||||
let result = run_fixture("fileio_negative.php", "readFile", Cap::FILE_IO, 14);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"fileio_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_fileio_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("fileio_adversarial.php", "readFile", Cap::FILE_IO, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_fileio_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("fileio_unsupported.php");
|
||||
let mut d = make_diag(&path, "serve", Cap::FILE_IO, 8);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── SSRF fixtures ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn php_ssrf_positive_is_confirmed() {
|
||||
let result = run_fixture("ssrf_positive.php", "fetchUrl", Cap::SSRF, 9);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"ssrf_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_ssrf_negative_is_not_confirmed() {
|
||||
let result = run_fixture("ssrf_negative.php", "fetchUrl", Cap::SSRF, 14);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"ssrf_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_ssrf_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("ssrf_adversarial.php", "fetchUrl", Cap::SSRF, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_ssrf_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("ssrf_unsupported.php");
|
||||
let mut d = make_diag(&path, "fetch", Cap::SSRF, 8);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── XSS fixtures ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn php_xss_positive_is_confirmed() {
|
||||
let result = run_fixture("xss_positive.php", "renderPage", Cap::HTML_ESCAPE, 8);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"xss_positive must be Confirmed; got {:?} (detail: {:?})",
|
||||
result.status,
|
||||
result.detail
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_xss_negative_is_not_confirmed() {
|
||||
let result = run_fixture("xss_negative.php", "renderPage", Cap::HTML_ESCAPE, 9);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed,
|
||||
"xss_negative must be NotConfirmed; got {:?}",
|
||||
result.status
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_xss_adversarial_is_oracle_collision() {
|
||||
let result = run_fixture("xss_adversarial.php", "renderPage", Cap::HTML_ESCAPE, 999);
|
||||
if result.status == VerifyStatus::Unsupported
|
||||
&& result.reason == Some(UnsupportedReason::BackendUnavailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
assert_eq!(result.status, VerifyStatus::Inconclusive);
|
||||
assert_eq!(
|
||||
result.inconclusive_reason,
|
||||
Some(InconclusiveReason::OracleCollisionSuspected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_xss_unsupported_is_confidence_too_low() {
|
||||
let path = fixture_path("xss_unsupported.php");
|
||||
let mut d = make_diag(&path, "render", Cap::HTML_ESCAPE, 8);
|
||||
d.confidence = Some(Confidence::Low);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&d, &opts);
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_diag(path: &Path, func: &str, cap: Cap, sink_line: u32) -> Diag {
|
||||
let path_str = path.to_string_lossy().into_owned();
|
||||
let evidence = Evidence {
|
||||
flow_steps: vec![
|
||||
FlowStep {
|
||||
step: 1,
|
||||
kind: FlowStepKind::Source,
|
||||
file: path_str.clone(),
|
||||
line: 1,
|
||||
col: 0,
|
||||
snippet: None,
|
||||
variable: Some("payload".into()),
|
||||
callee: None,
|
||||
function: Some(func.to_owned()),
|
||||
is_cross_file: false,
|
||||
},
|
||||
FlowStep {
|
||||
step: 2,
|
||||
kind: FlowStepKind::Sink,
|
||||
file: path_str.clone(),
|
||||
line: sink_line,
|
||||
col: 4,
|
||||
snippet: None,
|
||||
variable: None,
|
||||
callee: None,
|
||||
function: None,
|
||||
is_cross_file: false,
|
||||
},
|
||||
],
|
||||
sink_caps: cap.bits(),
|
||||
..Default::default()
|
||||
};
|
||||
Diag {
|
||||
path: path_str,
|
||||
line: sink_line as usize,
|
||||
col: 0,
|
||||
severity: Severity::High,
|
||||
id: "taint-unsanitised-flow".into(),
|
||||
category: FindingCategory::Security,
|
||||
path_validated: false,
|
||||
guard_kind: None,
|
||||
message: None,
|
||||
labels: vec![],
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(evidence),
|
||||
rank_score: None,
|
||||
rank_reason: None,
|
||||
suppressed: false,
|
||||
suppression: None,
|
||||
rollup: None,
|
||||
finding_id: String::new(),
|
||||
alternative_finding_ids: vec![],
|
||||
stable_hash: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -277,6 +277,218 @@ fn main() {
|
|||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── JS repro tests ───────────────────────────────────────────────────────
|
||||
|
||||
fn make_confirmed_js_spec(spec_hash: &str) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "js_determ000001".into(),
|
||||
entry_file: "tests/dynamic_fixtures/js/sqli_positive.js".into(),
|
||||
entry_name: "login".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::JavaScript,
|
||||
toolchain_id: "node-20".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "tests/dynamic_fixtures/js/sqli_positive.js".into(),
|
||||
sink_line: 8,
|
||||
spec_hash: spec_hash.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn js_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
|
||||
let spec = make_confirmed_js_spec("js_determ000001a");
|
||||
let opts = SandboxOptions::default();
|
||||
let outcome = make_confirmed_outcome();
|
||||
let verdict = make_confirmed_verdict("js_determ000001");
|
||||
let entry_src = "function login(username) { console.log(username); }\n";
|
||||
|
||||
let artifact1 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"// harness js\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("first JS repro write");
|
||||
let json1 =
|
||||
std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
std::fs::remove_dir_all(&artifact1.root).unwrap();
|
||||
|
||||
let artifact2 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"// harness js\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("second JS repro write");
|
||||
let json2 =
|
||||
std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
assert_eq!(json1, json2, "JS outcome.json must be byte-identical across two writes");
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── Go repro tests ───────────────────────────────────────────────────────
|
||||
|
||||
fn make_confirmed_go_spec(spec_hash: &str) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "go_determ000001".into(),
|
||||
entry_file: "tests/dynamic_fixtures/go/sqli_positive.go".into(),
|
||||
entry_name: "Login".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Go,
|
||||
toolchain_id: "go-1.21".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "tests/dynamic_fixtures/go/sqli_positive.go".into(),
|
||||
sink_line: 12,
|
||||
spec_hash: spec_hash.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
|
||||
let spec = make_confirmed_go_spec("go_determ000001a");
|
||||
let opts = SandboxOptions::default();
|
||||
let outcome = make_confirmed_outcome();
|
||||
let verdict = make_confirmed_verdict("go_determ000001");
|
||||
let entry_src = "package entry\nfunc Login(username string) {}\n";
|
||||
|
||||
let artifact1 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"// harness go\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("first Go repro write");
|
||||
let json1 =
|
||||
std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
std::fs::remove_dir_all(&artifact1.root).unwrap();
|
||||
|
||||
let artifact2 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"// harness go\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("second Go repro write");
|
||||
let json2 =
|
||||
std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
assert_eq!(json1, json2, "Go outcome.json must be byte-identical across two writes");
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── Java repro tests ─────────────────────────────────────────────────────
|
||||
|
||||
fn make_confirmed_java_spec(spec_hash: &str) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "java_determ00001".into(),
|
||||
entry_file: "tests/dynamic_fixtures/java/sqli_positive.java".into(),
|
||||
entry_name: "login".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Java,
|
||||
toolchain_id: "java-21".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "tests/dynamic_fixtures/java/sqli_positive.java".into(),
|
||||
sink_line: 9,
|
||||
spec_hash: spec_hash.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
|
||||
let spec = make_confirmed_java_spec("java_determ00001a");
|
||||
let opts = SandboxOptions::default();
|
||||
let outcome = make_confirmed_outcome();
|
||||
let verdict = make_confirmed_verdict("java_determ00001");
|
||||
let entry_src = "public class Entry { public static void login(String u) {} }\n";
|
||||
|
||||
let artifact1 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"// NyxHarness.java\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("first Java repro write");
|
||||
let json1 =
|
||||
std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
std::fs::remove_dir_all(&artifact1.root).unwrap();
|
||||
|
||||
let artifact2 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"// NyxHarness.java\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("second Java repro write");
|
||||
let json2 =
|
||||
std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
assert_eq!(json1, json2, "Java outcome.json must be byte-identical across two writes");
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── PHP repro tests ──────────────────────────────────────────────────────
|
||||
|
||||
fn make_confirmed_php_spec(spec_hash: &str) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "php_determ000001".into(),
|
||||
entry_file: "tests/dynamic_fixtures/php/sqli_positive.php".into(),
|
||||
entry_name: "login".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Php,
|
||||
toolchain_id: "php-8".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "tests/dynamic_fixtures/php/sqli_positive.php".into(),
|
||||
sink_line: 9,
|
||||
spec_hash: spec_hash.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn php_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
|
||||
let spec = make_confirmed_php_spec("php_determ000001a");
|
||||
let opts = SandboxOptions::default();
|
||||
let outcome = make_confirmed_outcome();
|
||||
let verdict = make_confirmed_verdict("php_determ000001");
|
||||
let entry_src = "<?php\nfunction login($username) {}\n";
|
||||
|
||||
let artifact1 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"<?php // harness\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("first PHP repro write");
|
||||
let json1 =
|
||||
std::fs::read_to_string(artifact1.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
std::fs::remove_dir_all(&artifact1.root).unwrap();
|
||||
|
||||
let artifact2 = repro::write(
|
||||
&spec, &opts, &outcome, &verdict,
|
||||
"<?php // harness\n", entry_src,
|
||||
b"' UNION SELECT 'NYX_SQL_CONFIRMED'--", "sqli-union-nyx", None,
|
||||
).expect("second PHP repro write");
|
||||
let json2 =
|
||||
std::fs::read_to_string(artifact2.root.join("expected/outcome.json")).unwrap();
|
||||
|
||||
assert_eq!(json1, json2, "PHP outcome.json must be byte-identical across two writes");
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
/// Verify verdict.json is correctly structured.
|
||||
#[test]
|
||||
fn verdict_json_is_valid() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue