{"record":{"id":"f42c92aea9a39d40","repo":"siyuan-note/siyuan","slug":"boot-appearance-asset-forbidden","errorCode":null,"errorMessage":"boot appearance asset forbidden","messagePattern":"boot appearance asset forbidden","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/boot_appearance.go","lineNumber":59,"sourceCode":"\tbootAppearanceSchemaVersion = 1\n\tbootAppearanceDirName       = \"boot-appearances\"\n\tbootAppearanceConfigName    = \"boot-appearance.json\"\n\tbootAppearanceManifestName  = \"boot.json\"\n\n\tmaxBootAppearanceManifestSize = 200 * 1024\n\tmaxBootAppearanceStyleSize    = 200 * 1024\n\tmaxBootAppearanceImageSize    = 5 * 1024 * 1024\n\tmaxBootAppearanceVideoSize    = 20 * 1024 * 1024\n\tmaxBootAppearanceTotalSize    = 50 * 1024 * 1024\n\tmaxBootAppearanceLayers       = 8\n\tmaxBootAppearanceEntries      = 256\n\tmaxBootAppearancePathDepth    = 16\n\tmaxBootAppearancePathLength   = 512\n)\n\nvar (\n\tErrBootAppearanceNotFound       = errors.New(\"boot appearance not found\")\n\tErrBootAppearanceAssetForbidden = errors.New(\"boot appearance asset forbidden\")\n\n\tbootAppearanceIDPattern    = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)\n\tbootAppearanceColorPattern = regexp.MustCompile(`^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$`)\n\tbootAppearanceConfLock     sync.RWMutex\n)\n\n// BootAppearanceSelection 表示当前工作空间选择的启动页外观。\ntype BootAppearanceSelection struct {\n\tSchemaVersion int    `json:\"schemaVersion\"`\n\tProvider      string `json:\"provider\"`\n\tAppearance    string `json:\"appearance\"`\n}\n\n// BootAppearance 描述已经校验且可安全交给启动页渲染的外观。\ntype BootAppearance struct {\n\tEnabled         bool                      `json:\"enabled\"`\n\tProvider        string                    `json:\"provider,omitempty\"`\n\tAppearance      string                    `json:\"appearance,omitempty\"`","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/boot_appearance.go#L41-L77","documentation":"ErrBootAppearanceAssetForbidden guards serving boot appearance asset files. ResolveBootAppearanceAsset refuses paths whose provider/appearance don't match the persisted selection, and validateBootAppearanceResource rejects files that escape the appearance directory (filepath.Rel fails or the relative path leaves the root) — blocking path traversal and cross-plugin asset access.","triggerScenarios":"Requesting a boot appearance asset for a plugin/appearance that is not the currently selected one (line 264); requesting a path that resolves outside the appearance directory via ../ traversal or symlinks (line 526).","commonSituations":"Crafted or stale URLs pointing at another plugin's assets; paths containing '..' segments or absolute components; symlinks inside an appearance dir pointing outward; serving cached asset URLs after the selection changed.","solutions":["Only request assets under the currently selected provider/appearance (re-resolve after any selection change)","Ensure request paths are relative and contain no '..' or leading '/' components","Remove or avoid symlinks escaping the appearance directory","Clear stale selection state via SetBootAppearance(\"\") so resolution matches the assets being served"],"exampleFix":"// before\nurl := \"/boot-appearance/other-plugin/other-appearance/logo.svg\" // mismatch or traversal\n// after\nsel := model.GetSelectedBootAppearance()\nurl := fmt.Sprintf(\"/boot-appearance/%s/%s/logo.svg\", sel.Provider, sel.Appearance)","handlingStrategy":"validation","validationCode":"clean := func(p string) bool { return p == path.Clean(\"/\"+p)[1:] && !strings.Contains(p, \"..\") }\nif !clean(assetPath) { return errors.New(\"illegal asset path\") }","typeGuard":"func safeRelPath(p string) bool {\n  rel, err := filepath.Rel(base, filepath.Join(base, p)); return err == nil && rel == p && !strings.HasPrefix(rel, \"..\")\n}","tryCatchPattern":"data, err := model.ResolveBootAppearanceAsset(p, id, rel)\nif errors.Is(err, model.ErrBootAppearanceAssetForbidden) {\n  http.Error(w, \"forbidden\", http.StatusForbidden)\n}","preventionTips":["Only use asset URLs derived from the current selection","Reject '..' and absolute segments in user-supplied paths","Avoid symlinks inside appearance directories","Regenerate asset URLs after any selection change"],"tags":["go","kernel","security","path-traversal","boot-appearance"],"backgroundTag":"path-traversal-blocked","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}