@@ -94,27 +94,26 @@ export function createOpenService(options?: OpenServiceOptions): DevframeService
9494 // Option sets from multiple installers merge via devframe's default
9595 // deep-merge: `roots` union, `editor` last-wins.
9696 setup ( ctx , { options } ) {
97- // Canonicalize each allowed root once (resolving symlinks in the root
98- // paths themselves) so containment compares canonical to canonical.
99- const allowedRoots = Promise . all (
100- [ ctx . workspaceRoot , ...( options ?. roots ?? [ ] ) ] . map ( r => nearestExistingCanonical ( resolve ( r ) ) ) ,
101- )
97+ const allowedRoots = [ ctx . workspaceRoot , ...( options ?. roots ?? [ ] ) ] . map ( r => resolve ( r ) )
98+ // Canonical forms of the same roots (symlinks in the root paths
99+ // resolved), computed once for the symlink-aware pass.
100+ const canonicalRoots = Promise . all ( allowedRoots . map ( r => nearestExistingCanonical ( r ) ) )
101+
102+ const within = ( roots : string [ ] , p : string ) : boolean => roots . some ( ( root ) => {
103+ const rel = relative ( root , p )
104+ return rel === '' || ( ! rel . startsWith ( '..' ) && ! isAbsolute ( rel ) )
105+ } )
102106
103107 /**
104- * Resolve `path` (relative paths against `workspaceRoot`) and assert
105- * its canonical location lands inside one of the allowed roots, or
106- * throw. Canonicalizing the nearest existing ancestor rejects a symlink
107- * that would redirect the open outside every allowed root, while still
108+ * Resolve `path` (relative paths against `workspaceRoot`) and assert it
109+ * lands inside one of the allowed roots, or throw. The lexical pass
110+ * rejects plain `..`/absolute escapes; the canonical pass rejects a
111+ * symlink that would redirect the open outside every root, while still
108112 * allowing not-yet-existing files under a root.
109113 */
110114 async function assertAllowedPath ( path : string ) : Promise < string > {
111115 const resolved = isAbsolute ( path ) ? resolve ( path ) : resolve ( ctx . workspaceRoot , path )
112- const canonical = await nearestExistingCanonical ( resolved )
113- const contained = ( await allowedRoots ) . some ( ( root ) => {
114- const rel = relative ( root , canonical )
115- return rel === '' || ( ! rel . startsWith ( '..' ) && ! isAbsolute ( rel ) )
116- } )
117- if ( ! contained )
116+ if ( ! within ( allowedRoots , resolved ) || ! within ( await canonicalRoots , await nearestExistingCanonical ( resolved ) ) )
118117 throw diagnostics . DS_OPEN_0002 ( { path } )
119118 return resolved
120119 }
0 commit comments