V7 nightly - #133
Conversation
…dated getters/setters and removed deprecated variable
…he set value, not only the default
…g (for use with -file start arguments)
…fallback to Stationeers game server app ID
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
…ing for socket operations
… of running backend plugins.
…isk to handle & stop existing plugins if redownload = true
…s, and unify plugin handling
| w.WriteHeader(resp.StatusCode) | ||
|
|
||
| // Copy the response body | ||
| _, err = io.Copy(w, resp.Body) |
Check warning
Code scanning / CodeQL
Reflected cross-site scripting Medium
This autofix suggestion was applied.
Show autofix suggestion
Hide autofix suggestion
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/htmlor similar), escape dangerous data before sending to the client usinghtml.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 theContent-Typeresponse header. - If the response is
text/html(or a recognizable HTML-like mime type), read the body, escape it usinghtml.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 insrc/api/pluginproxy/socketproxy.go, within the block reading the response and writing it to the HTTP client.
| @@ -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) | ||
| } | ||
| } | ||
| } | ||
| } |
|
|
||
| // 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
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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) |
| // 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 autofix suggestion was applied.
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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) |
| } | ||
|
|
||
| // Create or overwrite the file | ||
| file, err := os.Create(saveFilePath) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in path expression High
Show autofix suggestion
Hide autofix suggestion
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.
| @@ -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 { |
| // 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 autofix suggestion was applied.
Show autofix suggestion
Hide autofix suggestion
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
isValidRunfileIdentifierfunction tosrc/config/setters.govalidating the identifier string. - Call it from
SetRunfileIdentifierin addition to the whitespace check. - Update
SetRunfileIdentifierto 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.
| @@ -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() |
…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>
…rrently loaded runfile Identifier.
opened this to run codeQL