• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mybatis / migrations / 705

28 Apr 2025 04:49PM UTC coverage: 80.624% (-0.2%) from 80.873%
705

Pull #445

github

web-flow
Potential fix for code scanning alert no. 1: Arbitrary file access during archive extraction ("Zip Slip")

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Pull Request #445: Potential fix for code scanning alert no. 1: Arbitrary file access during archive extraction ("Zip Slip")

692 of 990 branches covered (69.9%)

0 of 7 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

1835 of 2276 relevant lines covered (80.62%)

0.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

38.3
/src/main/java/org/apache/ibatis/migration/io/DefaultVFS.java
1
/*
2
 *    Copyright 2010-2023 the original author or authors.
3
 *
4
 *    Licensed under the Apache License, Version 2.0 (the "License");
5
 *    you may not use this file except in compliance with the License.
6
 *    You may obtain a copy of the License at
7
 *
8
 *       https://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 *    Unless required by applicable law or agreed to in writing, software
11
 *    distributed under the License is distributed on an "AS IS" BASIS,
12
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 *    See the License for the specific language governing permissions and
14
 *    limitations under the License.
15
 */
16
package org.apache.ibatis.migration.io;
17

18
import java.io.BufferedReader;
19
import java.io.File;
20
import java.io.FileNotFoundException;
21
import java.io.IOException;
22
import java.io.InputStream;
23
import java.io.InputStreamReader;
24
import java.io.UnsupportedEncodingException;
25
import java.net.MalformedURLException;
26
import java.net.URL;
27
import java.net.URLEncoder;
28
import java.nio.file.InvalidPathException;
29
import java.util.ArrayList;
30
import java.util.Arrays;
31
import java.util.List;
32
import java.util.jar.JarEntry;
33
import java.util.jar.JarInputStream;
34
import java.util.logging.Level;
35
import java.util.logging.Logger;
36

37
/**
38
 * A default implementation of {@link VFS} that works for most application servers.
39
 *
40
 * @author Ben Gunter
41
 */
42
public class DefaultVFS extends VFS {
1✔
43
  private static final Logger log = Logger.getLogger(DefaultVFS.class.getName());
1✔
44

45
  /** The magic header that indicates a JAR (ZIP) file. */
46
  private static final byte[] JAR_MAGIC = { 'P', 'K', 3, 4 };
1✔
47

48
  @Override
49
  public boolean isValid() {
50
    return true;
1✔
51
  }
52

53
  @Override
54
  public List<String> list(URL url, String path) throws IOException {
55
    InputStream is = null;
1✔
56
    try {
57
      List<String> resources = new ArrayList<>();
1✔
58

59
      // First, try to find the URL of a JAR file containing the requested resource. If a JAR
60
      // file is found, then we'll list child resources by reading the JAR.
61
      URL jarUrl = findJarForResource(url);
1✔
62
      if (jarUrl != null) {
1!
63
        is = jarUrl.openStream();
×
64
        if (log.isLoggable(Level.FINER)) {
×
65
          log.log(Level.FINER, "Listing " + url);
×
66
        }
67
        resources = listResources(new JarInputStream(is), path);
×
68
      } else {
69
        List<String> children = new ArrayList<>();
1✔
70
        try {
71
          if (isJar(url)) {
1!
72
            // Some versions of JBoss VFS might give a JAR stream even if the resource
73
            // referenced by the URL isn't actually a JAR
74
            is = url.openStream();
×
75
            try (JarInputStream jarInput = new JarInputStream(is)) {
×
76
              if (log.isLoggable(Level.FINER)) {
×
77
                log.log(Level.FINER, "Listing " + url);
×
78
              }
79
              for (JarEntry entry; (entry = jarInput.getNextJarEntry()) != null;) {
×
80
                if (log.isLoggable(Level.FINER)) {
×
81
                  log.log(Level.FINER, "Jar entry: " + entry.getName());
×
82
                }
NEW
83
                String entryName = entry.getName();
×
NEW
84
                File entryFile = new File(path, entryName).getCanonicalFile();
×
NEW
85
                File baseDir = new File(path).getCanonicalFile();
×
NEW
86
                if (!entryFile.toPath().startsWith(baseDir.toPath())) {
×
NEW
87
                  if (log.isLoggable(Level.WARNING)) {
×
NEW
88
                    log.log(Level.WARNING, "Skipping potentially unsafe entry: " + entryName);
×
89
                  }
90
                  continue;
91
                }
NEW
92
                children.add(entryName);
×
UNCOV
93
              }
×
94
            }
95
          } else {
96
            /*
97
             * Some servlet containers allow reading from directory resources like a text file, listing the child
98
             * resources one per line. However, there is no way to differentiate between directory and file resources
99
             * just by reading them. To work around that, as each line is read, try to look it up via the class loader
100
             * as a child of the current resource. If any line fails then we assume the current resource is not a
101
             * directory.
102
             */
103
            is = url.openStream();
1✔
104
            List<String> lines = new ArrayList<>();
1✔
105
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) {
1✔
106
              for (String line; (line = reader.readLine()) != null;) {
1✔
107
                if (log.isLoggable(Level.FINER)) {
1!
108
                  log.log(Level.FINER, "Reader entry: " + line);
×
109
                }
110
                lines.add(line);
1✔
111
                if (getResources(path + "/" + line).isEmpty()) {
1✔
112
                  lines.clear();
1✔
113
                  break;
1✔
114
                }
115
              }
116
            } catch (InvalidPathException e) {
×
117
              // #1974
118
              lines.clear();
×
119
            }
1✔
120
            if (!lines.isEmpty()) {
1✔
121
              if (log.isLoggable(Level.FINER)) {
1!
122
                log.log(Level.FINER, "Listing " + url);
×
123
              }
124
              children.addAll(lines);
1✔
125
            }
126
          }
127
        } catch (FileNotFoundException e) {
×
128
          /*
129
           * For file URLs the openStream() call might fail, depending on the servlet container, because directories
130
           * can't be opened for reading. If that happens, then list the directory directly instead.
131
           */
132
          if (!"file".equals(url.getProtocol())) {
×
133
            // No idea where the exception came from so rethrow it
134
            throw e;
×
135
          }
136
          File file = new File(url.getFile());
×
137
          if (log.isLoggable(Level.FINER)) {
×
138
            log.log(Level.FINER, "Listing directory " + file.getAbsolutePath());
×
139
          }
140
          if (file.isDirectory()) {
×
141
            if (log.isLoggable(Level.FINER)) {
×
142
              log.log(Level.FINER, "Listing " + url);
×
143
            }
144
            children = Arrays.asList(file.list());
×
145
          }
146
        }
1✔
147

148
        // The URL prefix to use when recursively listing child resources
149
        String prefix = url.toExternalForm();
1✔
150
        if (!prefix.endsWith("/")) {
1!
151
          prefix = prefix + "/";
1✔
152
        }
153

154
        // Iterate over immediate children, adding files and recurring into directories
155
        for (String child : children) {
1✔
156
          String resourcePath = path + "/" + child;
1✔
157
          resources.add(resourcePath);
1✔
158
          URL childUrl = new URL(prefix + child);
1✔
159
          resources.addAll(list(childUrl, resourcePath));
1✔
160
        }
1✔
161
      }
162

163
      return resources;
1✔
164
    } finally {
165
      if (is != null) {
1!
166
        try {
167
          is.close();
1✔
168
        } catch (Exception e) {
×
169
          // Ignore
170
        }
1✔
171
      }
172
    }
173
  }
174

175
  /**
176
   * List the names of the entries in the given {@link JarInputStream} that begin with the specified {@code path}.
177
   * Entries will match with or without a leading slash.
178
   *
179
   * @param jar
180
   *          The JAR input stream
181
   * @param path
182
   *          The leading path to match
183
   *
184
   * @return The names of all the matching entries
185
   *
186
   * @throws IOException
187
   *           If I/O errors occur
188
   */
189
  protected List<String> listResources(JarInputStream jar, String path) throws IOException {
190
    // Include the leading and trailing slash when matching names
191
    if (!path.startsWith("/")) {
×
192
      path = '/' + path;
×
193
    }
194
    if (!path.endsWith("/")) {
×
195
      path = path + '/';
×
196
    }
197

198
    // Iterate over the entries and collect those that begin with the requested path
199
    List<String> resources = new ArrayList<>();
×
200
    for (JarEntry entry; (entry = jar.getNextJarEntry()) != null;) {
×
201
      if (!entry.isDirectory()) {
×
202
        // Add leading slash if it's missing
203
        StringBuilder name = new StringBuilder(entry.getName());
×
204
        if (name.charAt(0) != '/') {
×
205
          name.insert(0, '/');
×
206
        }
207

208
        // Check file name
209
        if (name.indexOf(path) == 0) {
×
210
          if (log.isLoggable(Level.FINER)) {
×
211
            log.log(Level.FINER, "Found resource: " + name);
×
212
          }
213
          // Trim leading slash
214
          resources.add(name.substring(1));
×
215
        }
216
      }
×
217
    }
218
    return resources;
×
219
  }
220

221
  /**
222
   * Attempts to deconstruct the given URL to find a JAR file containing the resource referenced by the URL. That is,
223
   * assuming the URL references a JAR entry, this method will return a URL that references the JAR file containing the
224
   * entry. If the JAR cannot be located, then this method returns null.
225
   *
226
   * @param url
227
   *          The URL of the JAR entry.
228
   *
229
   * @return The URL of the JAR file, if one is found. Null if not.
230
   *
231
   * @throws MalformedURLException
232
   *           the malformed URL exception
233
   */
234
  protected URL findJarForResource(URL url) throws MalformedURLException {
235
    if (log.isLoggable(Level.FINER)) {
1!
236
      log.log(Level.FINER, "Find JAR URL: " + url);
×
237
    }
238

239
    // If the file part of the URL is itself a URL, then that URL probably points to the JAR
240
    boolean continueLoop = true;
1✔
241
    while (continueLoop) {
1✔
242
      try {
243
        url = new URL(url.getFile());
×
244
        if (log.isLoggable(Level.FINER)) {
×
245
          log.log(Level.FINER, "Inner URL: " + url);
×
246
        }
247
      } catch (MalformedURLException e) {
1✔
248
        // This will happen at some point and serves as a break in the loop
249
        continueLoop = false;
1✔
250
      }
1✔
251
    }
252

253
    // Look for the .jar extension and chop off everything after that
254
    StringBuilder jarUrl = new StringBuilder(url.toExternalForm());
1✔
255
    int index = jarUrl.lastIndexOf(".jar");
1✔
256
    if (index < 0) {
1!
257
      if (log.isLoggable(Level.FINER)) {
1!
258
        log.log(Level.FINER, "Not a JAR: " + jarUrl);
×
259
      }
260
      return null;
1✔
261
    }
262
    jarUrl.setLength(index + 4);
×
263
    if (log.isLoggable(Level.FINER)) {
×
264
      log.log(Level.FINER, "Extracted JAR URL: " + jarUrl);
×
265
    }
266

267
    // Try to open and test it
268
    try {
269
      URL testUrl = new URL(jarUrl.toString());
×
270
      if (isJar(testUrl)) {
×
271
        return testUrl;
×
272
      }
273
      // WebLogic fix: check if the URL's file exists in the filesystem.
274
      if (log.isLoggable(Level.FINER)) {
×
275
        log.log(Level.FINER, "Not a JAR: " + jarUrl);
×
276
      }
277
      jarUrl.replace(0, jarUrl.length(), testUrl.getFile());
×
278
      File file = new File(jarUrl.toString());
×
279

280
      // File name might be URL-encoded
281
      if (!file.exists()) {
×
282
        try {
283
          file = new File(URLEncoder.encode(jarUrl.toString(), "UTF-8"));
×
284
        } catch (UnsupportedEncodingException e) {
×
285
          throw new RuntimeException("Unsupported encoding?  UTF-8?  That's impossible.");
×
286
        }
×
287
      }
288

289
      if (file.exists()) {
×
290
        if (log.isLoggable(Level.FINER)) {
×
291
          log.log(Level.FINER, "Trying real file: " + file.getAbsolutePath());
×
292
        }
293
        testUrl = file.toURI().toURL();
×
294
        if (isJar(testUrl)) {
×
295
          return testUrl;
×
296
        }
297
      }
298
    } catch (MalformedURLException e) {
×
299
      log.log(Level.WARNING, "Invalid JAR URL: " + jarUrl);
×
300
    }
×
301

302
    if (log.isLoggable(Level.FINER)) {
×
303
      log.log(Level.FINER, "Not a JAR: " + jarUrl);
×
304
    }
305
    return null;
×
306
  }
307

308
  /**
309
   * Converts a Java package name to a path that can be looked up with a call to
310
   * {@link ClassLoader#getResources(String)}.
311
   *
312
   * @param packageName
313
   *          The Java package name to convert to a path
314
   *
315
   * @return the package path
316
   */
317
  protected String getPackagePath(String packageName) {
318
    return packageName == null ? null : packageName.replace('.', '/');
×
319
  }
320

321
  /**
322
   * Returns true if the resource located at the given URL is a JAR file.
323
   *
324
   * @param url
325
   *          The URL of the resource to test.
326
   *
327
   * @return true, if is jar
328
   */
329
  protected boolean isJar(URL url) {
330
    return isJar(url, new byte[JAR_MAGIC.length]);
1✔
331
  }
332

333
  /**
334
   * Returns true if the resource located at the given URL is a JAR file.
335
   *
336
   * @param url
337
   *          The URL of the resource to test.
338
   * @param buffer
339
   *          A buffer into which the first few bytes of the resource are read. The buffer must be at least the size of
340
   *          {@link #JAR_MAGIC}. (The same buffer may be reused for multiple calls as an optimization.)
341
   *
342
   * @return true, if is jar
343
   */
344
  protected boolean isJar(URL url, byte[] buffer) {
345
    try (InputStream is = url.openStream()) {
1✔
346
      is.read(buffer, 0, JAR_MAGIC.length);
1✔
347
      if (Arrays.equals(buffer, JAR_MAGIC)) {
1!
348
        if (log.isLoggable(Level.FINER)) {
×
349
          log.log(Level.FINER, "Found JAR: " + url);
×
350
        }
351
        return true;
×
352
      }
353
    } catch (Exception e) {
×
354
      // Failure to read the stream means this is not a JAR
355
    }
1✔
356

357
    return false;
1✔
358
  }
359
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc