-
Notifications
You must be signed in to change notification settings - Fork 4
π‘οΈ Sentinel: [CRITICAL] Fix path traversal in plugin manager #95
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| ## 2024-05-23 - Prevent Path Traversal in Plugin Paths | ||
| **Vulnerability:** Plugin names parsed from untrusted metadata (`# @name`) or extracted from direct URLs were passed unsanitized into `filepath.Join`, allowing path traversal (e.g., writing or deleting arbitrary files outside the plugin directory). | ||
| **Learning:** `filepath.Join` cleans paths but evaluates `../` sequences internally. Thus, combining a base directory with untrusted input containing `../` still allows escaping the base directory. | ||
| **Prevention:** Sanitize untrusted input using `filepath.Base(filepath.Clean("/" + name))` *before* joining it with base directories to explicitly restrict it to a single filename. |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,6 +31,12 @@ func NewManager() *Manager { | |||||||||||
| } | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // sanitizePluginName ensures the given name cannot escape the target directory | ||||||||||||
| // by stripping path components. | ||||||||||||
| func sanitizePluginName(name string) string { | ||||||||||||
| return filepath.Base(filepath.Clean("/" + name)) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Discover finds all installed plugins (both scripts and binaries) | ||||||||||||
| func (m *Manager) Discover() ([]Plugin, error) { | ||||||||||||
| if err := os.MkdirAll(m.pluginDir, 0755); err != nil { | ||||||||||||
|
|
@@ -365,7 +371,7 @@ func (m *Manager) addBinaryPlugin(owner, repo, pluginName string) error { | |||||||||||
| return err | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| destPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s", pluginName)) | ||||||||||||
| destPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s", sanitizePluginName(pluginName))) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize the
Suggested change
|
||||||||||||
|
|
||||||||||||
| // Check if already installed | ||||||||||||
| if err := m.checkExistingPlugin(destPath, pluginName); err != nil { | ||||||||||||
|
|
@@ -441,7 +447,7 @@ func (m *Manager) addScriptPlugin(owner, repo, pluginName string) error { | |||||||||||
| return err | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| destPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s.sh", meta.Name)) | ||||||||||||
| destPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s.sh", sanitizePluginName(meta.Name))) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize the
Suggested change
|
||||||||||||
|
|
||||||||||||
| if err := m.checkExistingPlugin(destPath, meta.Name); err != nil { | ||||||||||||
| return err | ||||||||||||
|
|
@@ -492,9 +498,9 @@ func (m *Manager) addFromDirectURL(url string) error { | |||||||||||
|
|
||||||||||||
| var destPath string | ||||||||||||
| if isScript { | ||||||||||||
| destPath = filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s.sh", pluginName)) | ||||||||||||
| destPath = filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s.sh", sanitizePluginName(pluginName))) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize
Suggested change
|
||||||||||||
| } else { | ||||||||||||
| destPath = filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s", pluginName)) | ||||||||||||
| destPath = filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s", sanitizePluginName(pluginName))) | ||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| if err := m.checkExistingPlugin(destPath, pluginName); err != nil { | ||||||||||||
|
|
@@ -784,8 +790,8 @@ func (m *Manager) updateScriptPlugin(p Plugin, client *http.Client) error { | |||||||||||
| // Remove uninstalls a plugin (handles both binaries and scripts) | ||||||||||||
| func (m *Manager) Remove(name string) error { | ||||||||||||
| // Try binary first, then script | ||||||||||||
| binaryPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s", name)) | ||||||||||||
| scriptPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s.sh", name)) | ||||||||||||
| binaryPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s", sanitizePluginName(name))) | ||||||||||||
| scriptPath := filepath.Join(m.pluginDir, fmt.Sprintf("prism-plugin-%s.sh", sanitizePluginName(name))) | ||||||||||||
|
Comment on lines
+793
to
+794
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sanitize the
Suggested change
|
||||||||||||
|
|
||||||||||||
| var path string | ||||||||||||
| if _, err := os.Stat(binaryPath); err == nil { | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation returns a path separator (e.g.,
/or\) if the inputnameis empty or resolves to the root (e.g.,..). This results in an invalid filename likeprism-plugin-/, which will cause subsequent file operations to fail with an "invalid argument" or "is a directory" error. Consider returning a safe default name or an empty string in these cases to ensure a valid filename is always generated.