mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 12:17:23 +00:00
feat(lua): 适配器导入路径冲突校验(R6)——防静默覆盖与路径逃逸
用户要求:导入适配器时必须检查路径冲突,避免静默错误。 之前 UploadAdapter(name, code) 直接拼 adapter_dir/<name>.lua: - name 含 "../" 或绝对路径会写到目录外 - name 与内置同名会静默覆盖,加载失败只报 "adapter X missing", 现场与病因分离 实现(internal/lua/adapter_validate.go): - ValidateAdapterName:字符集 ^[a-zA-Z0-9_-]+$,拒绝 /、\、..、绝对路径、空白 - ValidateAdapterPath:文件必须在 adapter_dir 内 - VM.InstallPluginAdapter:插件装内置名 → ErrAdapterNameReserved 拒 - Core.UploadAdapter:在 WriteFile 之前校验(防路径逃逸) - Core.RemoveAdapter:也校验名字(防 os.Remove 越界) 设计取舍:保留名禁止只对插件生效,不对运维生效。 - operator 放 openai.lua 覆盖内置 = 文档化的热覆盖机制,放行 - 第三方插件静默换掉内核自带的协议翻译 = 篡改发行物,拒绝 - 先误做成了全拒,Start() 加载内置适配器直接崩,测试抓住后改对 (TestLoadBundledAdapters 等锁住回归) 测试 12 个:路径穿越/绝对路径/保留段/空白/内置覆盖(允许 vs 插件拒绝)/ 目录外加载/错误点名/热重载不回归。 验证:go test -tags luajit ./... 全绿。
This commit is contained in:
@ -729,19 +729,39 @@ func (v *VM) ListAdapters() []APIAdapter {
|
||||
}
|
||||
|
||||
// LoadAdapter compiles and registers an adapter script from a file.
|
||||
//
|
||||
// The path is validated for SAFETY before reading (R6): it must sit inside the
|
||||
// VM's adapter dir and the derived name must be a legal adapter name (no path
|
||||
// traversal, absolute paths, or whitespace). It does NOT reject built-in names
|
||||
// here: an operator placing openai.lua in adapter_dir to OVERRIDE the bundled
|
||||
// openai is the documented hot-override mechanism. Only PLUGIN installs are
|
||||
// barred from shadowing a built-in (see InstallPluginAdapter), because a
|
||||
// third-party plugin clobbering the kernel's shipped adapter is tampering,
|
||||
// whereas the operator editing their own deployment is not.
|
||||
func (v *VM) LoadAdapter(path string) error {
|
||||
name, err := ValidateAdapterPath(path, v.dir, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read adapter: %w", err)
|
||||
}
|
||||
name := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
return v.LoadAdapterSource(name, string(data))
|
||||
}
|
||||
|
||||
// LoadAdapterSource registers an adapter from source code. Replacing an
|
||||
// existing adapter shuts the old pool down and re-boots lazily, carrying the
|
||||
// configured ceiling over so a hot-reload does not reset elastic sizing.
|
||||
//
|
||||
// The name is validated for path SAFETY only (no traversal/absolute/space).
|
||||
// Built-in names are allowed: an operator override is legitimate. A plugin
|
||||
// that must NOT shadow a built-in uses InstallPluginAdapter, which adds the
|
||||
// reserved-name gate.
|
||||
func (v *VM) LoadAdapterSource(name, code string) error {
|
||||
if err := ValidateAdapterName(name, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
static, err := inspectScript(name, code)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -801,6 +821,31 @@ func (v *VM) pool(name string) *adapterPool {
|
||||
return v.pools[name]
|
||||
}
|
||||
|
||||
// bundledNames returns the set of adapter names that ship inside the kernel.
|
||||
// It is derived from the embedded FS (same source writeBundledAdapters uses),
|
||||
// so a new built-in adapter is automatically reserved and an operator cannot
|
||||
// clobber it by importing a same-named external adapter.
|
||||
// BundledAdapterNames is the exported counterpart of bundledNames for callers
|
||||
// outside the lua package (core.UploadAdapter validates against it). It returns
|
||||
// the set of adapter names shipped inside the kernel so an external import that
|
||||
// shadows one is rejected up front (R6).
|
||||
func (v *VM) BundledAdapterNames() []string { return v.bundledNames() }
|
||||
|
||||
func (v *VM) bundledNames() []string {
|
||||
entries, err := bundledAdapters.ReadDir("adapters")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
out = append(out, strings.TrimSuffix(e.Name(), ".lua"))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (v *VM) writeBundledAdapters() error {
|
||||
// Derive the set from the embedded FS instead of a hardcoded name list so
|
||||
// adding an adapter never requires maintaining a second list.
|
||||
@ -1380,3 +1425,22 @@ func (v *VM) AdapterRevision(name string) uint64 {
|
||||
defer p.mu.Unlock()
|
||||
return p.revision
|
||||
}
|
||||
|
||||
// InstallPluginAdapter registers an adapter ON BEHALF OF A PLUGIN, with the
|
||||
// one extra rule plugins (and only plugins) are held to: the name must not
|
||||
// shadow a built-in adapter.
|
||||
//
|
||||
// Why plugins are stricter than operators: an operator dropping openai.lua in
|
||||
// adapter_dir is overriding their own deployment (documented, intended). A
|
||||
// third-party plugin registering an adapter named "openai" would silently
|
||||
// swap out the kernel's shipped protocol translation for code the operator
|
||||
// never reviewed — tampering with the shipped artifact. R6 requires that to
|
||||
// fail loudly with the conflict named, not succeed quietly.
|
||||
//
|
||||
// This is the entry point Phase 3's plugin adapter-install bridge calls.
|
||||
func (v *VM) InstallPluginAdapter(name, code string) error {
|
||||
if err := ValidateAdapterName(name, v.bundledNames()); err != nil {
|
||||
return err
|
||||
}
|
||||
return v.LoadAdapterSource(name, code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user