@@ -24,6 +24,7 @@ pub const ImportValidationError = error{
2424 ImportAliasRequired ,
2525 DuplicateImportAlias ,
2626 RelativeImportMustIncludeOraExtension ,
27+ RelativeImportOutsideAllowedRoots ,
2728 InvalidImportSpecifier ,
2829 PackageRootConflict ,
2930 ParseFailed ,
@@ -279,7 +280,6 @@ const Resolver = struct {
279280 };
280281
281282 const canonical_id = std .fmt .allocPrint (self .allocator , "file:{s}" , .{resolved_path }) catch {
282- self .allocator .free (resolved_path );
283283 return ImportValidationError .OutOfMemory ;
284284 };
285285
@@ -325,6 +325,7 @@ const Resolver = struct {
325325 }
326326
327327 try self .states .put (record .canonical_id , .visiting );
328+ errdefer _ = self .states .remove (record .canonical_id );
328329 try self .stack .append (self .allocator , record .canonical_id );
329330 defer _ = self .stack .pop ();
330331
@@ -407,6 +408,16 @@ const Resolver = struct {
407408 std .log .warn ("Import target not found: '{s}' in module '{s}' ({s})" , .{ specifier , importer .resolved_path , @errorName (err ) });
408409 return ImportValidationError .ImportTargetNotFound ;
409410 };
411+ errdefer self .allocator .free (resolved_path );
412+
413+ if (! try self .isAllowedRelativePath (resolved_path )) {
414+ std .log .warn ("Relative import escapes allowed roots: '{s}' resolved to '{s}' from '{s}'" , .{
415+ specifier ,
416+ resolved_path ,
417+ importer .resolved_path ,
418+ });
419+ return ImportValidationError .RelativeImportOutsideAllowedRoots ;
420+ }
410421
411422 const canonical_id = std .fmt .allocPrint (self .allocator , "file:{s}" , .{resolved_path }) catch {
412423 self .allocator .free (resolved_path );
@@ -420,6 +431,42 @@ const Resolver = struct {
420431 };
421432 }
422433
434+ fn isPathWithinRoot (path : []const u8 , root : []const u8 ) bool {
435+ if (! std .mem .startsWith (u8 , path , root )) return false ;
436+ if (path .len == root .len ) return true ;
437+ if (root .len == 0 ) return false ;
438+ if (root [root .len - 1 ] == std .fs .path .sep ) return true ;
439+ return path [root .len ] == std .fs .path .sep ;
440+ }
441+
442+ fn isAllowedRelativePath (self : * Resolver , resolved_path : []const u8 ) ImportValidationError ! bool {
443+ if (self .options .workspace_roots .len == 0 and self .options .include_roots .len == 0 ) {
444+ const cwd_root = std .fs .cwd ().realpathAlloc (self .allocator , "." ) catch {
445+ return ImportValidationError .OutOfMemory ;
446+ };
447+ defer self .allocator .free (cwd_root );
448+ return isPathWithinRoot (resolved_path , cwd_root );
449+ }
450+
451+ for (self .options .workspace_roots ) | root | {
452+ const real_root = std .fs .cwd ().realpathAlloc (self .allocator , root ) catch {
453+ continue ;
454+ };
455+ defer self .allocator .free (real_root );
456+ if (isPathWithinRoot (resolved_path , real_root )) return true ;
457+ }
458+
459+ for (self .options .include_roots ) | root | {
460+ const real_root = std .fs .cwd ().realpathAlloc (self .allocator , root ) catch {
461+ continue ;
462+ };
463+ defer self .allocator .free (real_root );
464+ if (isPathWithinRoot (resolved_path , real_root )) return true ;
465+ }
466+
467+ return false ;
468+ }
469+
423470 fn parsePackageSpecifier (specifier : []const u8 ) ImportValidationError ! PackageSpecifier {
424471 const slash_index = std .mem .indexOfScalar (u8 , specifier , '/' ) orelse return ImportValidationError .InvalidImportSpecifier ;
425472 if (slash_index == 0 or slash_index + 1 >= specifier .len ) {
0 commit comments