{"record":{"id":"9e1b2aac718a8670","repo":"iOfficeAI/AionUi","slug":"path-traversal-blocked-relativepath-resolves","errorCode":null,"errorMessage":"Path traversal blocked: \"${relativePath}\" resolves outside workspace","messagePattern":"Path traversal blocked: \"(.+?)\" resolves outside workspace","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/desktop/src/renderer/pages/conversation/Preview/components/renderers/HTMLRenderer.tsx","lineNumber":131,"sourceCode":" * @returns 绝对路径 / Absolute path\n */\nexport function resolveRelativePath(basePath: string, relativePath: string, workspace?: string): string {\n  // 去除协议前缀 / Remove protocol prefix\n  const cleanBasePath = basePath.replace(/^file:\\/\\//, '');\n  const baseDir =\n    cleanBasePath.substring(0, cleanBasePath.lastIndexOf('/') + 1) ||\n    cleanBasePath.substring(0, cleanBasePath.lastIndexOf('\\\\') + 1);\n\n  // 如果相对路径已经是绝对路径 / If relative path is already absolute\n  if (relativePath.startsWith('/') || /^[a-zA-Z]:/.test(relativePath)) {\n    // 先归一化 `.`/`..` 段再做边界检查，避免形如 `<workspace>/../../secret`\n    // 的字面遍历：它以工作区前缀开头却实际逃逸，未归一时会骗过 isWithinRoot。\n    // Normalize `.`/`..` before the boundary check: a literal\n    // `<workspace>/../../secret` starts with the workspace prefix yet escapes\n    // it, so an un-normalized string would slip past isWithinRoot.\n    const normalizedAbsolute = normalizeAbsolute(relativePath);\n    if (workspace && !isWithinRoot(workspace, normalizedAbsolute)) {\n      throw new Error(`Path traversal blocked: \"${relativePath}\" resolves outside workspace`);\n    }\n    return normalizedAbsolute;\n  }\n\n  // 处理 ./ 和 ../ / Handle ./ and ../\n  const parts = baseDir.replace(/\\\\/g, '/').split('/').filter(Boolean);\n  const relParts = relativePath.replace(/\\\\/g, '/').split('/');\n\n  for (const part of relParts) {\n    if (part === '..') {\n      parts.pop();\n    } else if (part !== '.') {\n      parts.push(part);\n    }\n  }\n\n  // 保留 Windows 盘符格式 / Preserve Windows drive letter format\n  const result = /^[a-zA-Z]:/.test(baseDir) ? parts.join('/') : '/' + parts.join('/');","sourceCodeStart":113,"sourceCodeEnd":149,"githubUrl":"https://github.com/iOfficeAI/AionUi/blob/711aa0550ee183ea495dc33e2c05c7943b70a60a/packages/desktop/src/renderer/pages/conversation/Preview/components/renderers/HTMLRenderer.tsx#L113-L149","documentation":"Thrown by resolveRelativePath in HTMLRenderer when a resource path referenced by previewed HTML, after normalization of '.'/'..' segments, resolves to an absolute location outside the trusted workspace root. It is a deliberate security guard against path traversal, not an unexpected failure.","triggerScenarios":"Calling absolutePath/resourcePath with a value like '<workspace>/../../secret.txt' or a symlink-style relative chain that normalizes outside the workspace prefix; any <img src>/<link href> in the previewed HTML whose resolved target leaves the root.","commonSituations":"Generated HTML artifacts that reference assets via ../../ pointing above the artifact directory, hand-authored HTML with absolute paths from another machine, or a workspace root misconfigured (empty/incorrect) so legitimate paths fail the isWithinRoot check.","solutions":["Inspect the offending ${relativePath} from the error message and rewrite the reference so it stays under the workspace root","If the asset legitimately lives outside the workspace, copy it into the workspace (or an assets/ subfolder) before preview","Verify the workspace value passed to the renderer matches where the HTML artifact actually resides","Sanitize generated HTML: normalize and re-root relative URLs at generation time"],"exampleFix":"<!-- before -->\n<img src=\"../../../shared/logo.png\" />\n<!-- after: move asset into workspace and reference relatively -->\n<img src=\"./assets/logo.png\" />","handlingStrategy":"validation","validationCode":"const normalized = path.normalize(rawPath).replace(/\\\\/g, '/');\nconst root = path.normalize(workspace + '/');\nif (normalized !== root && !normalized.startsWith(root)) {\n  // reject before calling absolutePath/resourcePath\n  throw new Error(`Unsafe resource path: ${rawPath}`);\n}","typeGuard":"const isWithinWorkspace = (p: string, root: string): boolean => {\n  const n = path.normalize(p).replace(/\\\\/g, '/');\n  const r = path.normalize(root + '/').replace(/\\\\/g, '/');\n  return n === r || n.startsWith(r);\n};","tryCatchPattern":"try {\n  const abs = absolutePath(src);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Path traversal blocked')) {\n    // drop the resource or serve a placeholder, never crash the preview\n  } else throw e;\n}","preventionTips":["Normalize and re-root all URLs from generated HTML at generation time","Keep preview artifacts self-contained under the workspace","Treat traversal errors as resource-dropped, not fatal"],"tags":["path-traversal","security","preview","html","workspace"],"backgroundTag":"path-traversal-blocked","analyzedSha":"711aa0550ee183ea495dc33e2c05c7943b70a60a","analyzedAt":"2026-08-28T07:56:06.558Z","schemaVersion":2},"datasetVersion":"2026-08-28T11:17:15.048Z"}