fix(@angular-devkit/schematics): prevent schematic writes from escaping the workspace via symlinks#33334
Conversation
…ng the workspace via symlinks A schematic/migration write can escape the workspace root via a symlinked directory inside the workspace: ScopedHost's containment is lexical and does not resolve symlinks. WorkspaceRootHost resolves the real (symlink-collapsed) path and rejects any write/delete/rename whose real location is outside the workspace root, mirroring the MCP host's realpath-based restriction.
There was a problem hiding this comment.
Code Review
This pull request introduces WorkspaceRootHost, a custom ScopedHost that prevents schematic operations (write, delete, rename) from escaping the workspace root via symlinks by resolving real paths. The review feedback highlights a potential crash during initial workspace creation (e.g., ng new) when the root directory does not exist yet, as realpathSync will throw an ENOENT error. To resolve this, the reviewer suggests extracting a robust resolveRealPath helper that walks up to the first existing ancestor, and importing basename to support this helper.
Sorry, something went wrong.
…ce-root containment The previous WorkspaceRootHost resolved the workspace root with realpathSync(getSystemPath(root)) in the constructor, which throws ENOENT when the root directory does not exist yet — e.g. during `ng new`, which creates the workspace — crashing the workflow. Extract a resolveRealPath helper that walks up to the first existing ancestor, resolves its real path, and re-appends the remaining non-existent segments. Use it for both the workspace root and the asserted target path, so containment works for not-yet-created files and a not-yet-created root while still rejecting symlink escapes.
|
Good catch — fixed. |
Sorry, something went wrong.
Reopens #33325 (the previous PR was accidentally closed; this is the same single-file fix rebased cleanly onto current
main).A schematic / migration write can escape the workspace root via a symlinked directory inside the workspace. The schematics
NodeWorkflowwrites throughvirtualFs.ScopedHost, whose containment is lexical (it joins the path under the root but does not resolve symlinks), and the schematicsTreeonly rejects... So a workspace that contains a symlinked directory can route awrite/delete/renameto a file outside the workspace.Fix
WorkspaceRootHost extends virtualFs.ScopedHostresolves the real (symlink-collapsed) path withrealpathSync(walking up to the first existing ancestor for not-yet-created files) and rejects anywrite/delete/renamewhose real location is outside the workspace root (rel === ".." || rel.startsWith(".." + sep) || isAbsolute(rel)). This mirrors the realpath-based root restriction already used by the MCP host (createRootRestrictedHost). No change for legitimate in-workspace paths.Single file changed:
packages/angular_devkit/schematics/tools/workflow/node-workflow.ts(+70 −1).