1515import java .net .MalformedURLException ;
1616import java .net .URL ;
1717import java .net .URLClassLoader ;
18+ import java .nio .file .Files ;
1819import java .nio .file .Path ;
20+ import java .nio .file .Paths ;
1921import java .time .Instant ;
2022import java .util .*;
2123import java .util .concurrent .ConcurrentHashMap ;
@@ -83,53 +85,57 @@ public LoadedPlugin loadPlugin(Path jarPath, Plugin dbPlugin)
8385 updateLoaderState (pluginId , Plugin .LoaderState .LOADING , null );
8486
8587 try {
86- // Validate jarPath is a local file under the plugins directory
87- // (prevent SSRF via external URLs and path traversal)
88- Path normalized = jarPath .normalize ();
89- Path pluginsDir = Path .of (
88+ // Validate jarPath is a local canonical file under the plugins directory
89+ // (prevent SSRF/path confusion via traversal or symlink escape)
90+ Path pluginsDir = Paths .get (
9091 System .getenv ().getOrDefault (
9192 "SYNAPSE_HOME" ,
9293 System .getProperty ("user.home" ) + "/.synapse"
9394 ),
9495 "plugins"
95- ).normalize ();
96- if (
97- !normalized .isAbsolute () || !normalized .startsWith (pluginsDir )
98- ) {
96+ );
97+ Path normalized = jarPath .normalize ();
98+ if (!normalized .isAbsolute ()) {
9999 throw new PluginLoadException (
100100 pluginId ,
101101 "Plugin JAR path must be an absolute local file under " +
102102 pluginsDir
103103 );
104104 }
105- if (!java . nio . file . Files .exists (normalized )) {
105+ if (!Files .exists (normalized )) {
106106 throw new PluginLoadException (
107107 pluginId ,
108108 "Plugin JAR not found: " + normalized
109109 );
110110 }
111111
112- // Build file:// URL directly from validated local path.
113- // CodeQL flags toUri().toURL() as SSRF-prone, so we construct
114- // the URL string manually from the already-validated path.
115- String absolutePath = normalized .toAbsolutePath ().toString ();
116- String fileUrl = "file://" + absolutePath .replace ('\\' , '/' );
112+ Path realPluginsDir ;
113+ Path realJarPath ;
114+ try {
115+ realPluginsDir = pluginsDir .toRealPath ();
116+ realJarPath = normalized .toRealPath ();
117+ } catch (java .io .IOException e ) {
118+ throw new PluginLoadException (
119+ pluginId ,
120+ "Failed to resolve canonical plugin path: " + e .getMessage (),
121+ e
122+ );
123+ }
117124
118- // Validate the URL string starts with file:// before creating URL
119- if (!fileUrl .startsWith ("file://" )) {
125+ if (!realJarPath .startsWith (realPluginsDir ) || !Files .isRegularFile (realJarPath )) {
120126 throw new PluginLoadException (
121127 pluginId ,
122- "Plugin JAR URL must start with file://"
128+ "Plugin JAR path must be a regular file under " + realPluginsDir
123129 );
124130 }
125131
126132 URL jarUrl ;
127133 try {
128- jarUrl = new URL ( fileUrl );
134+ jarUrl = realJarPath . toUri (). toURL ( );
129135 } catch (MalformedURLException e ) {
130136 throw new PluginLoadException (
131137 pluginId ,
132- "Invalid JAR URL: " + fileUrl
138+ "Invalid JAR URL: " + realJarPath
133139 );
134140 }
135141
@@ -140,7 +146,7 @@ public LoadedPlugin loadPlugin(Path jarPath, Plugin dbPlugin)
140146 );
141147
142148 // Layer 2: Create JPMS ModuleLayer
143- ModuleFinder pluginFinder = ModuleFinder .of (normalized );
149+ ModuleFinder pluginFinder = ModuleFinder .of (realJarPath );
144150 Set <ModuleDescriptor > descriptors = pluginFinder
145151 .findAll ()
146152 .stream ()
0 commit comments