{"record":{"id":"fb13e5601c696003","repo":"siyuan-note/siyuan","slug":"errbootappearanceassetforbidden","errorCode":"ErrBootAppearanceAssetForbidden","errorMessage":"boot appearance asset forbidden","messagePattern":"boot appearance asset forbidden","errorType":"error_code","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/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/boot_appearance.go#L41-L77","documentation":"A boot appearance asset request was refused because the requested provider/appearance (or file path) does not match the currently validated selection, or the resolved path escapes the appearance directory. ResolveBootAppearanceAsset rejects requests whose provider or appearance id differs from the persisted selection (boot_appearance.go:264), and validateBootAppearanceResource returns it when filepath.Rel indicates a path outside the appearance directory (path traversal, boot_appearance.go:526). This is a security boundary protecting against serving arbitrary kernel files.","triggerScenarios":"Requesting a boot appearance asset URL with a provider or appearance id that differs from the currently persisted selection; requesting asset paths containing ../ or absolute path components that escape the appearance directory; calling validateBootAppearanceResource with a symlink-resolved path outside the package directory.","commonSituations":"Stale frontend references assets from a previously selected appearance after the user switched selections; crafted URLs attempting directory traversal; symlinked files inside the appearance package pointing outside.","solutions":["Serve/request assets only for the currently selected provider and appearance (re-read the selection and regenerate asset URLs).","Remove path traversal sequences and use only relative asset paths within the package.","Replace or remove symlinks in the appearance package that point outside its directory.","Reload the boot page after changing the selection so cached old-provider asset URLs are not used."],"exampleFix":"// before\nresolve(\"/themes/other-provider/assets/logo.png\") // provider mismatch\n// after\nsel := model.ResolveBootAppearanceSelection()\nasset, err := model.ResolveBootAppearanceAsset(sel.Provider, sel.Appearance, \"assets/logo.png\")","handlingStrategy":"validation","validationCode":"function assetPathIsSafe(p) { return !p.includes('..') && !path.isAbsolute(p) }","typeGuard":null,"tryCatchPattern":"asset, err := model.ResolveBootAppearanceAsset(provider, id, rel)\nif errors.Is(err, model.ErrBootAppearanceAssetForbidden) {\n    http.Error(w, \"forbidden\", http.StatusForbidden)\n}","preventionTips":["Always derive asset URLs from the current persisted selection","Reject or normalize paths containing traversal segments before requesting","Avoid symlinks inside appearance packages"],"tags":["security","path-traversal","boot-appearance"],"backgroundTag":"path-traversal-blocked","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}