bertramdev / asset-pipeline

The core implementation of the asset pipeline for the jvm
193 stars 91 forks source link

Can't prevent directory listing #262

Closed jrAtAustin closed 4 years ago

jrAtAustin commented 4 years ago

In development mode I get the following exception for URI http://localhost:8080/assets/, in production I get the full directory listing. My app is a Grails 2.5.6 app (in production we deploy to Jetty 9.1.). 2020-06-09 15:48:26,120 ERROR StandardWrapperValve - Servlet.service() for servlet [gsp] in context with path [] threw exception java.lang.ArrayIndexOutOfBoundsException: Negative array index [-1] too large for array size 0 at org.codehaus.groovy.runtime.dgmimpl.arrays.ArrayMetaMethod.normaliseIndex(ArrayMetaMethod.java:37) at org.codehaus.groovy.runtime.dgmimpl.arrays.ObjectArrayGetAtMetaMethod$MyPojoMetaMethodSite.call(ObjectArrayGetAtMetaMethod.java:60) at asset.pipeline.AssetHelper.extensionFromURI(AssetHelper.groovy:112) at asset.pipeline.AssetHelper$extensionFromURI$0.call(Unknown Source) at asset.pipeline.fs.FileSystemAssetResolver.getAsset(FileSystemAssetResolver.groovy:71) at asset.pipeline.fs.AssetResolver$getAsset$0.call(Unknown Source) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at asset.pipeline.fs.AssetResolver$getAsset$0.call(Unknown Source) at asset.pipeline.AssetHelper.fileForUri(AssetHelper.groovy:50) at asset.pipeline.AssetHelper.fileForUri(AssetHelper.groovy) at asset.pipeline.AssetHelper$fileForUri$3.call(Unknown Source) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at asset.pipeline.AssetHelper$fileForUri$3.call(Unknown Source) at asset.pipeline.AssetPipeline.serveAsset(AssetPipeline.groovy:31) at asset.pipeline.AssetPipeline$serveAsset$6.call(Unknown Source) at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:48) at asset.pipeline.AssetPipeline$serveAsset$6.call(Unknown Source) at asset.pipeline.grails.AssetPipelineFilter.doFilter(AssetPipelineFilter.groovy:170) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.codehaus.groovy.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:67) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at com.mrhaki.grails.plugin.xframeoptions.web.XFrameOptionsFilter.doFilterInternal(XFrameOptionsFilter.java:69) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344) at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344) at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1070) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:611) at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:314) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:748)

jrAtAustin commented 4 years ago

In class AssetHelper method extensionFromURI() there is a bug. If uri is '/' uriComponents.length will be 0 causing the java.lang.ArrayIndexOutOfBoundsException here String lastUriComponent = uriComponents[uriComponents.length - 1]

    /**
     * Obtains the extension for the given URI
     *
     * @param uri The URI
     * @return The extension or null
     */
    static String extensionFromURI(String uri) {
        String[] uriComponents = uri.split("/")
        String lastUriComponent = uriComponents[uriComponents.length - 1]
        List<String> extensions = (List<String>) (AssetHelper.assetSpecs.collect { Class<AssetFile> it -> it.extensions }.flatten().sort(false) { String a, String b -> -(a.size()) <=> -(b.size()) })
        String extension = null
        extension = extensions.find { lastUriComponent.endsWith(".${it}".toString()) }
        if (!extension) {
            if (lastUriComponent.lastIndexOf(".") >= 0) {
                extension = uri.substring(uri.lastIndexOf(".") + 1)
            }
        }

        return extension
    }

The ArrayIndexOutOfBoundsException exception only occurs in development mode, in production the directory is listed which is considered a directory indexing security issue.

jrAtAustin commented 4 years ago

The latest version of AssetHelper. extensionFromURI() seems to be fixed. I will try to include a later version of the asset-pipeline-code in my Grails 2.5.6 project.

    /**
     * Obtains the extension for the given URI
     *
     * @param uri The URI
     * @return The extension or null
     */
    static String extensionFromURI(String uri) {
        String[] uriComponents = uri.split("/")
        if (uriComponents.length == 0) {
            return null
        }
        String lastUriComponent = uriComponents[uriComponents.length - 1]
        List<String> extensions = (List<String>) (AssetHelper.assetSpecs.collect { Class<AssetFile> it -> it.extensions }.flatten().sort(false) { String a, String b -> -(a.size()) <=> -(b.size()) })
        String extension = null
        extension = extensions.find { lastUriComponent.endsWith(".${it}".toString()) }
        if (!extension) {
            if (lastUriComponent.lastIndexOf(".") >= 0) {
                extension = uri.substring(uri.lastIndexOf(".") + 1)
            }
        }

        return extension
    }
jrAtAustin commented 4 years ago

I worked around this issue by excluding the asset-pipeline-core library from the asset-pipeline plugin and including a newer version of the asset-pipeline-core library.

runtime 'com.bertramlabs.plugins:asset-pipeline-core:2.15.1'
...
compile "org.grails.plugins:asset-pipeline:2.14.1.1", {
   excludes 'asset-pipeline-core:2.14.1'
}

In production this produces the following exception but does not display the directory contents.

java.io.FileNotFoundException: Could not open ServletContext resource [/assets/]
        at org.springframework.web.context.support.ServletContextResource.getInputStream(ServletContextResource.java:141)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:93)
        at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:325)
        at groovy.lang.MetaClassImpl.getProperty(MetaClassImpl.java:1855)

It would be preferable if 403 Forbidden was returned.