Skip to content

V7 nightly - #133

Closed
JacksonTheMaster wants to merge 93 commits into
nightlyfrom
v7-nightly
Closed

V7 nightly#133
JacksonTheMaster wants to merge 93 commits into
nightlyfrom
v7-nightly

Conversation

@JacksonTheMaster

@JacksonTheMaster JacksonTheMaster commented Oct 22, 2025

Copy link
Copy Markdown
Member

opened this to run codeQL

…dated getters/setters and removed deprecated variable
Add support for generic BepInEx without SSCM
change BepInEx setup logic
refactor SSCM integration across relevant files
add better arg printing in startup for debugging
switched to only supporting runfiles in v6
removed running steamcmd during startup entirely
hooked up runfile arg builder fully
this should have been multiple commits, whoops
@JacksonTheMaster
JacksonTheMaster marked this pull request as ready for review October 22, 2025 17:44
@JacksonTheMaster
JacksonTheMaster changed the base branch from main to nightly October 22, 2025 17:44
w.WriteHeader(resp.StatusCode)

// Copy the response body
_, err = io.Copy(w, resp.Body)

Check warning

Code scanning / CodeQL

Reflected cross-site scripting Medium

Cross-site scripting vulnerability due to
user-provided value
.
Cross-site scripting vulnerability due to
user-provided value
.
Cross-site scripting vulnerability due to
user-provided value
.
Cross-site scripting vulnerability due to
user-provided value
.

Copilot Autofix

AI 10 months ago

To mitigate the possibility of reflected cross-site scripting introduced by the proxy, the proxy should ensure it does not forward arbitrarily dangerous content from the backend to the client. One effective defense is to perform output encoding/escaping (especially for HTML content types) before writing them to the response. The handler should review the backend's Content-Type header:

  • For HTML content (text/html or similar), escape dangerous data before sending to the client using html.EscapeString.
  • For non-HTML content (JSON, images, etc.), output encoding may not be necessary, but one may consider blocking or restricting unexpected content types.

To implement this:

  • Before copying the response body to w, check the Content-Type response header.
  • If the response is text/html (or a recognizable HTML-like mime type), read the body, escape it using html.EscapeString, and then write the escaped string to the client.
  • For all other content types, use the existing logic (io.Copy).
  • Add the required import "html".
    These code changes should be made in src/api/pluginproxy/socketproxy.go, within the block reading the response and writing it to the HTTP client.

Suggested changeset 1
src/api/pluginproxy/socketproxy.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/api/pluginproxy/socketproxy.go b/src/api/pluginproxy/socketproxy.go
--- a/src/api/pluginproxy/socketproxy.go
+++ b/src/api/pluginproxy/socketproxy.go
@@ -6,7 +6,7 @@
 	"net"
 	"net/http"
 	"strings"
-
+	"html"
 	"github.com/SteamServerUI/SteamServerUI/v7/src/logger"
 )
 
@@ -78,10 +78,24 @@
 		// Set the status code
 		w.WriteHeader(resp.StatusCode)
 
-		// Copy the response body
-		_, err = io.Copy(w, resp.Body)
-		if err != nil {
-			logger.Plugin.Debugf("Failed to write response to client: %v", err)
+		// Copy the response body (with XSS mitigation for HTML responses)
+		contentType := resp.Header.Get("Content-Type")
+		if strings.HasPrefix(contentType, "text/html") {
+			bodyBytes, err := io.ReadAll(resp.Body)
+			if err != nil {
+				logger.Plugin.Debugf("Failed to read HTML response body: %v", err)
+			} else {
+				escaped := html.EscapeString(string(bodyBytes))
+				_, err = w.Write([]byte(escaped))
+				if err != nil {
+					logger.Plugin.Debugf("Failed to write escaped HTML response to client: %v", err)
+				}
+			}
+		} else {
+			_, err = io.Copy(w, resp.Body)
+			if err != nil {
+				logger.Plugin.Debugf("Failed to write response to client: %v", err)
+			}
 		}
 	}
 }
EOF
@@ -6,7 +6,7 @@
"net"
"net/http"
"strings"

"html"
"github.com/SteamServerUI/SteamServerUI/v7/src/logger"
)

@@ -78,10 +78,24 @@
// Set the status code
w.WriteHeader(resp.StatusCode)

// Copy the response body
_, err = io.Copy(w, resp.Body)
if err != nil {
logger.Plugin.Debugf("Failed to write response to client: %v", err)
// Copy the response body (with XSS mitigation for HTML responses)
contentType := resp.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "text/html") {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
logger.Plugin.Debugf("Failed to read HTML response body: %v", err)
} else {
escaped := html.EscapeString(string(bodyBytes))
_, err = w.Write([]byte(escaped))
if err != nil {
logger.Plugin.Debugf("Failed to write escaped HTML response to client: %v", err)
}
}
} else {
_, err = io.Copy(w, resp.Body)
if err != nil {
logger.Plugin.Debugf("Failed to write response to client: %v", err)
}
}
}
}
Copilot is powered by AI and may make mistakes. Always verify output.
@JacksonTheMaster JacksonTheMaster committed this autofix suggestion 10 months ago.

// get the dir of the saveFilePath, and os.MkdirAll it if it doesn't exist
dir := filepath.Dir(saveFilePath)
if _, err := os.Stat(dir); os.IsNotExist(err) {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

The best way to fix this is to validate the identifier argument to ensure it cannot be used to escape the intended directory or manipulate the path in an unintended way. Since the filename is formed as "run" + identifier + ".ssui", we should ensure that identifier does not contain:

  • Path separators (/ and \)
  • Parent directory references (..)
  • Absolute path starts (though the prefix mitigates this, but ambiguity is better removed)

A simple approach: check for the presence of /, \, or .. in the identifier and reject the operation if any are found. Optionally, use a regular expression or allow-list for what a valid identifier should look like (e.g., only alphanumeric and dashes/underscores). This validation should be performed as early as possible: ideally at the beginning of SaveRunfileToDisk. If the check fails, log the error and return an error.

No additional dependencies are required; standard library suffices.

Edit only the SaveRunfileToDisk function in src/steamserverui/gallery/runfilegallery.go, adding a check at the top of the function.

Suggested changeset 1
src/steamserverui/gallery/runfilegallery.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/steamserverui/gallery/runfilegallery.go b/src/steamserverui/gallery/runfilegallery.go
--- a/src/steamserverui/gallery/runfilegallery.go
+++ b/src/steamserverui/gallery/runfilegallery.go
@@ -89,6 +89,11 @@
 
 // saveRunfileToDisk downloads a runfile by identifier and saves it to RunfilesDir
 func SaveRunfileToDisk(identifier string) error {
+	// Validate identifier to prevent path traversal or injection
+	if strings.Contains(identifier, "/") || strings.Contains(identifier, "\\") || strings.Contains(identifier, "..") {
+		logger.Runfile.Error(fmt.Sprintf("Invalid runfile identifier: %s", identifier))
+		return fmt.Errorf("invalid runfile identifier")
+	}
 	filename := fmt.Sprintf("run%s.ssui", identifier)
 	baseURL := "https://steamserverui.github.io/runfiles"
 	fileURL := fmt.Sprintf("%s/%s", baseURL, filename)
EOF
@@ -89,6 +89,11 @@

// saveRunfileToDisk downloads a runfile by identifier and saves it to RunfilesDir
func SaveRunfileToDisk(identifier string) error {
// Validate identifier to prevent path traversal or injection
if strings.Contains(identifier, "/") || strings.Contains(identifier, "\\") || strings.Contains(identifier, "..") {
logger.Runfile.Error(fmt.Sprintf("Invalid runfile identifier: %s", identifier))
return fmt.Errorf("invalid runfile identifier")
}
filename := fmt.Sprintf("run%s.ssui", identifier)
baseURL := "https://steamserverui.github.io/runfiles"
fileURL := fmt.Sprintf("%s/%s", baseURL, filename)
Copilot is powered by AI and may make mistakes. Always verify output.
// get the dir of the saveFilePath, and os.MkdirAll it if it doesn't exist
dir := filepath.Dir(saveFilePath)
if _, err := os.Stat(dir); os.IsNotExist(err) {
if err := os.MkdirAll(dir, 0755); err != nil {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

The best way to fix the issue is to validate the identifier before constructing the filename in SaveRunfileToDisk. Since the identifier is expected to represent a runfile "name" or key, we should ensure it is a simple segment, with no path separators (/, \) and no parent directory traversals (..). The validation should reject any identifier containing these characters or sequences, returning an error before proceeding with any file system actions.

To implement the fix, in src/steamserverui/gallery/runfilegallery.go in the SaveRunfileToDisk function, right at the beginning, check the identifier for invalid substrings and return an error if found. This can be done using the standard library strings.Contains. No additional imports beyond those already present are needed.

Suggested changeset 1
src/steamserverui/gallery/runfilegallery.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/steamserverui/gallery/runfilegallery.go b/src/steamserverui/gallery/runfilegallery.go
--- a/src/steamserverui/gallery/runfilegallery.go
+++ b/src/steamserverui/gallery/runfilegallery.go
@@ -89,6 +89,10 @@
 
 // saveRunfileToDisk downloads a runfile by identifier and saves it to RunfilesDir
 func SaveRunfileToDisk(identifier string) error {
+	// Validate identifier: reject if contains path separators or ".."
+	if strings.Contains(identifier, "/") || strings.Contains(identifier, "\\") || strings.Contains(identifier, "..") {
+		return fmt.Errorf("invalid identifier: path traversal or separator detected")
+	}
 	filename := fmt.Sprintf("run%s.ssui", identifier)
 	baseURL := "https://steamserverui.github.io/runfiles"
 	fileURL := fmt.Sprintf("%s/%s", baseURL, filename)
EOF
@@ -89,6 +89,10 @@

// saveRunfileToDisk downloads a runfile by identifier and saves it to RunfilesDir
func SaveRunfileToDisk(identifier string) error {
// Validate identifier: reject if contains path separators or ".."
if strings.Contains(identifier, "/") || strings.Contains(identifier, "\\") || strings.Contains(identifier, "..") {
return fmt.Errorf("invalid identifier: path traversal or separator detected")
}
filename := fmt.Sprintf("run%s.ssui", identifier)
baseURL := "https://steamserverui.github.io/runfiles"
fileURL := fmt.Sprintf("%s/%s", baseURL, filename)
Copilot is powered by AI and may make mistakes. Always verify output.
@JacksonTheMaster JacksonTheMaster committed this autofix suggestion 10 months ago.
}

// Create or overwrite the file
file, err := os.Create(saveFilePath)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the issue, the untrusted identifier parameter must be validated so that malicious input cannot result in arbitrary file paths. Since the filename is built as run<identifier>.ssui, reasonable constraints would be: the identifier should not contain any path separators (/ or \), parent directory references (..), or be empty. A stricter approach is to allow only alphanumeric characters, dashes, or underscores in the identifier (allow list). The validation should be done at the beginning of SaveRunfileToDisk(identifier string). If the identifier fails validation, the function should return an error and avoid any filesystem access.

Perform the validation right at the start of SaveRunfileToDisk, before using identifier in any way. Add an appropriate error log and return a user-friendly error if validation fails.

Introduce an internal helper function, e.g., isValidIdentifier, which checks (using either a regular expression or string checks) that the identifier is safe (only allowed characters, not empty).

Adjust imports to include "regexp" if using a regular expression.


Suggested changeset 1
src/steamserverui/gallery/runfilegallery.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/steamserverui/gallery/runfilegallery.go b/src/steamserverui/gallery/runfilegallery.go
--- a/src/steamserverui/gallery/runfilegallery.go
+++ b/src/steamserverui/gallery/runfilegallery.go
@@ -7,6 +7,7 @@
 	"net/http"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 
@@ -89,6 +90,12 @@
 
 // saveRunfileToDisk downloads a runfile by identifier and saves it to RunfilesDir
 func SaveRunfileToDisk(identifier string) error {
+	// Validate identifier: only allow alphanumeric, dash, and underscore, and must be non-empty
+	if !isValidIdentifier(identifier) {
+		logger.Runfile.Error(fmt.Sprintf("Invalid runfile identifier: %q", identifier))
+		return fmt.Errorf("invalid runfile identifier")
+	}
+
 	filename := fmt.Sprintf("run%s.ssui", identifier)
 	baseURL := "https://steamserverui.github.io/runfiles"
 	fileURL := fmt.Sprintf("%s/%s", baseURL, filename)
@@ -137,6 +144,16 @@
 	return nil
 }
 
+// isValidIdentifier checks if the identifier is a valid filename component
+// Only allows alphanumerics, dash, and underscore, and must be 1-64 chars long.
+func isValidIdentifier(s string) bool {
+	if len(s) == 0 || len(s) > 64 {
+		return false
+	}
+	matched, err := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, s)
+	return err == nil && matched
+}
+
 // compareVersions compares two semantic version strings (x.y.z)
 // Returns -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
 func compareVersions(v1, v2 string) int {
EOF
@@ -7,6 +7,7 @@
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"sync"

@@ -89,6 +90,12 @@

// saveRunfileToDisk downloads a runfile by identifier and saves it to RunfilesDir
func SaveRunfileToDisk(identifier string) error {
// Validate identifier: only allow alphanumeric, dash, and underscore, and must be non-empty
if !isValidIdentifier(identifier) {
logger.Runfile.Error(fmt.Sprintf("Invalid runfile identifier: %q", identifier))
return fmt.Errorf("invalid runfile identifier")
}

filename := fmt.Sprintf("run%s.ssui", identifier)
baseURL := "https://steamserverui.github.io/runfiles"
fileURL := fmt.Sprintf("%s/%s", baseURL, filename)
@@ -137,6 +144,16 @@
return nil
}

// isValidIdentifier checks if the identifier is a valid filename component
// Only allows alphanumerics, dash, and underscore, and must be 1-64 chars long.
func isValidIdentifier(s string) bool {
if len(s) == 0 || len(s) > 64 {
return false
}
matched, err := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, s)
return err == nil && matched
}

// compareVersions compares two semantic version strings (x.y.z)
// Returns -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
func compareVersions(v1, v2 string) int {
Copilot is powered by AI and may make mistakes. Always verify output.
// Write to file with retries
const maxRetries = 3
for attempt := 1; attempt <= maxRetries; attempt++ {
if err := os.WriteFile(filePath, data, 0644); err != nil {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
This path depends on a
user-provided value
.

Copilot Autofix

AI 10 months ago

To fix the problem, we must ensure that the value of RunfileIdentifier (sourced from HTTP input as the identifier field) cannot be used to construct malicious file paths. The safest approach is to validate that this identifier complies with strict criteria before saving files or using it as a component in file paths. This can be done by introducing a validation function IsValidRunfileIdentifier(string) bool that checks the identifier only contains allowed characters—e.g., alphanumeric, dash, and underscore, with no slashes, dots, spaces, or other potentially dangerous characters.

The place to insert validation is in SetRunfileIdentifier in src/config/setters.go, since all assignment to RunfileIdentifier is funneled through this setter. Alternatively, further validation could be placed in the handler or when saving, but the setter is the ideal choke point based on the code flow.

We will:

  • Add an isValidRunfileIdentifier function to src/config/setters.go validating the identifier string.
  • Call it from SetRunfileIdentifier in addition to the whitespace check.
  • Update SetRunfileIdentifier to reject identifiers containing invalid characters, and update the error messages accordingly.
  • Optionally, we can log a warning if an invalid value is detected (consistent with error handling style shown).

No new dependencies are needed. We can use regexp (already imported in args.go, but not in setters.go) for robust validation; if not available, a simple manual check using strings can be substituted.


Suggested changeset 1
src/config/setters.go
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/config/setters.go b/src/config/setters.go
--- a/src/config/setters.go
+++ b/src/config/setters.go
@@ -4,6 +4,7 @@
 	"fmt"
 	"strings"
 	"time"
+	"regexp"
 )
 
 // Although this is a not a real setter, this function can be used to save the config safely
@@ -86,19 +87,28 @@
 	return safeSaveConfig()
 }
 
-// SetRunfileGame sets the RunfileGame with validation
+// SetRunfileIdentifier sets the RunfileIdentifier with validation
 func SetRunfileIdentifier(value string) error {
 	ConfigMu.Lock()
 	defer ConfigMu.Unlock()
 
 	if strings.TrimSpace(value) == "" {
-		return fmt.Errorf("runfile game cannot be empty")
+		return fmt.Errorf("runfile identifier cannot be empty")
 	}
+	if !isValidRunfileIdentifier(value) {
+		return fmt.Errorf("invalid runfile identifier; must be alphanumeric, dash or underscore only")
+	}
 
 	RunfileIdentifier = value
 	return safeSaveConfig()
 }
 
+// isValidRunfileIdentifier checks that the identifier is safe to use as a file component
+func isValidRunfileIdentifier(s string) bool {
+	matched, err := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, s)
+	return err == nil && matched
+}
+
 // Debug and Logging Settings
 func SetIsDebugMode(value bool) error {
 	ConfigMu.Lock()
EOF
@@ -4,6 +4,7 @@
"fmt"
"strings"
"time"
"regexp"
)

// Although this is a not a real setter, this function can be used to save the config safely
@@ -86,19 +87,28 @@
return safeSaveConfig()
}

// SetRunfileGame sets the RunfileGame with validation
// SetRunfileIdentifier sets the RunfileIdentifier with validation
func SetRunfileIdentifier(value string) error {
ConfigMu.Lock()
defer ConfigMu.Unlock()

if strings.TrimSpace(value) == "" {
return fmt.Errorf("runfile game cannot be empty")
return fmt.Errorf("runfile identifier cannot be empty")
}
if !isValidRunfileIdentifier(value) {
return fmt.Errorf("invalid runfile identifier; must be alphanumeric, dash or underscore only")
}

RunfileIdentifier = value
return safeSaveConfig()
}

// isValidRunfileIdentifier checks that the identifier is safe to use as a file component
func isValidRunfileIdentifier(s string) bool {
matched, err := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, s)
return err == nil && matched
}

// Debug and Logging Settings
func SetIsDebugMode(value bool) error {
ConfigMu.Lock()
Copilot is powered by AI and may make mistakes. Always verify output.
@JacksonTheMaster JacksonTheMaster committed this autofix suggestion 10 months ago.
JacksonTheMaster and others added 3 commits October 22, 2025 19:50
…in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…ripting

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…in path expression

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants