├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc.go ├── examples ├── chi │ └── main.go ├── mux │ └── main.go └── static │ └── secret.txt ├── go.mod ├── unindexed.go └── unindexed_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - "1.x" 6 | - "tip" 7 | 8 | matrix: 9 | fast_finish: true 10 | 11 | install: 12 | - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). 13 | script: 14 | - go get -t -v ./... 15 | - diff -u <(echo -n) <(gofmt -d -s .) 16 | - go vet . 17 | - go test -v -race ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jordan Wright 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # unindexed 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/jordan-wright/unindexed)](https://goreportcard.com/report/github.com/jordan-wright/unindexed) [![GoDoc](https://godoc.org/github.com/gophish/gophish?status.svg)](https://godoc.org/github.com/gophish/gophish) ![[TravisCI](https://travis-ci.org/jordan-wright/unindexed.svg?branch=master)](https://travis-ci.org/jordan-wright/unindexed.svg?branch=master) 4 | 5 | A Golang HTTP FileSystem that disables directory indexing. 6 | 7 | ## Motivation 8 | 9 | By default, the `http.Dir` filesystem has [directory indexing enabled](https://www.owasp.org/index.php/OWASP_Periodic_Table_of_Vulnerabilities_-_Directory_Indexing). For example, let's say you have a `.git/` folder at the root of the folder you're serving. If someone were to request `your_url/.git/`, the contents of the folder would be listed. 10 | 11 | This package disables directory indexing, preventing the contents from being listed. 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get -u github.com/jordan-wright/unindexed 17 | ``` 18 | 19 | ## Usage 20 | 21 | The easiest way to use `unindexed` is as a drop-in replacement for `http.Dir`, which is commonly used to serve static files. 22 | 23 | Here's a simple example using the `gorilla/mux` router: 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "log" 30 | "net/http" 31 | 32 | "github.com/gorilla/mux" 33 | "github.com/jordan-wright/unindexed" 34 | ) 35 | 36 | func main() { 37 | router := mux.NewRouter() 38 | router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("../static"))) 39 | log.Fatal(http.ListenAndServe(":8080", router)) 40 | } 41 | ``` 42 | 43 | Other examples can be found in the `examples/` directory. -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package unindexed provides an HTTP filesystem that disables directory 2 | // indexing 3 | // 4 | // Motivation 5 | // 6 | // By default, the "http.Dir" filesystem has directory indexing enabled, which 7 | // means that if a directory is requested that doesn't include an index.html 8 | // file, a list of files in the directory is returned. This could leak 9 | // sensitive information and should be avoided unless needed. 10 | // 11 | // Usage 12 | // 13 | // The easiest way to use this package is through unindexed.Dir, which is a 14 | // drop-in replacement for http.Dir. If a directory is requested that doesn't 15 | // have an index.html file, this package returns a http.StatusNotFound response. 16 | package unindexed 17 | -------------------------------------------------------------------------------- /examples/chi/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/go-chi/chi" 8 | "github.com/jordan-wright/unindexed" 9 | ) 10 | 11 | func main() { 12 | router := chi.NewRouter() 13 | router.Handle("/*", http.FileServer(unindexed.Dir("../static/"))) 14 | log.Fatal(http.ListenAndServe(":8080", router)) 15 | } 16 | -------------------------------------------------------------------------------- /examples/mux/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | "github.com/jordan-wright/unindexed" 9 | ) 10 | 11 | func main() { 12 | router := mux.NewRouter() 13 | router.PathPrefix("/").Handler(http.FileServer(unindexed.Dir("../static"))) 14 | log.Fatal(http.ListenAndServe(":8080", router)) 15 | } 16 | -------------------------------------------------------------------------------- /examples/static/secret.txt: -------------------------------------------------------------------------------- 1 | This file shouldn't be listed if the /static/ folder is requested. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jordan-wright/unindexed 2 | -------------------------------------------------------------------------------- /unindexed.go: -------------------------------------------------------------------------------- 1 | package unindexed 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | // FileSystem is an implementation of a standard http.FileSystem 10 | // without the ability to list files in the directory. 11 | // This implementation is largely inspired by 12 | // https://www.alexedwards.net/blog/disable-http-fileserver-directory-listings 13 | type FileSystem struct { 14 | fs http.FileSystem 15 | } 16 | 17 | // Open returns a file from the static directory. If the requested path ends 18 | // with a slash, there is a check for an index.html file. If none exists, then 19 | // an os.ErrPermission error is returned, causing a 403 Forbidden error to be 20 | // returned to the client 21 | func (ufs FileSystem) Open(name string) (http.File, error) { 22 | f, err := ufs.fs.Open(name) 23 | if err != nil { 24 | return nil, err 25 | } 26 | // Check to see if what we opened was a directory. If it was, we will 27 | // return an error 28 | s, err := f.Stat() 29 | if s.IsDir() { 30 | index := strings.TrimSuffix(name, "/") + "/index.html" 31 | _, err := ufs.fs.Open(index) 32 | if err != nil { 33 | return nil, os.ErrPermission 34 | } 35 | } 36 | return f, nil 37 | } 38 | 39 | // Dir is a drop-in replacement for http.Dir, providing an unindexed 40 | // filesystem for serving static files. 41 | func Dir(filepath string) http.FileSystem { 42 | return FileSystem{ 43 | fs: http.Dir(filepath), 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /unindexed_test.go: -------------------------------------------------------------------------------- 1 | package unindexed_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/jordan-wright/unindexed" 14 | ) 15 | 16 | // The easiest way to use unindexed is to use the Dir function, which is 17 | // a drop-in replacement to http.Dir. 18 | func ExampleDir() { 19 | http.Handle("/", http.FileServer(unindexed.Dir("./static/"))) 20 | } 21 | 22 | func createTemporaryDirectory(t *testing.T) string { 23 | // Create the static directory 24 | dir, err := ioutil.TempDir(os.TempDir(), "unindexed-static") 25 | if err != nil { 26 | t.Fatalf("unable to create temp directory: %s", err) 27 | } 28 | // Add a secret file 29 | secret, err := ioutil.TempFile(dir, "secret.txt") 30 | if err != nil { 31 | t.Fatalf("unable to create secret file: %s", err) 32 | } 33 | fmt.Fprintf(secret, "This is a secret!") 34 | return dir 35 | } 36 | 37 | func tearDown(dir string, t *testing.T) { 38 | err := os.RemoveAll(dir) 39 | if err != nil { 40 | t.Fatalf("unable to remove temp directory: %s", err) 41 | } 42 | } 43 | 44 | func TestNotFound(t *testing.T) { 45 | dir := createTemporaryDirectory(t) 46 | defer tearDown(dir, t) 47 | r := httptest.NewRequest("GET", "/doesntexist", nil) 48 | w := httptest.NewRecorder() 49 | handler := http.FileServer(unindexed.Dir(dir)) 50 | handler.ServeHTTP(w, r) 51 | 52 | if w.Code != http.StatusNotFound { 53 | t.Fatalf("unexpected status received. expected %d got %d", http.StatusNotFound, w.Code) 54 | } 55 | } 56 | 57 | func TestNoIndex(t *testing.T) { 58 | dir := createTemporaryDirectory(t) 59 | defer tearDown(dir, t) 60 | r := httptest.NewRequest("GET", "/", nil) 61 | w := httptest.NewRecorder() 62 | handler := http.FileServer(unindexed.Dir(dir)) 63 | handler.ServeHTTP(w, r) 64 | 65 | if w.Code != http.StatusForbidden { 66 | t.Fatalf("unexpected status received. expected %d got %d", http.StatusForbidden, w.Code) 67 | } 68 | } 69 | 70 | func TestWithIndex(t *testing.T) { 71 | dir := createTemporaryDirectory(t) 72 | defer tearDown(dir, t) 73 | 74 | expectedBody := []byte("testing") 75 | 76 | err := ioutil.WriteFile(filepath.Join(dir, "index.html"), expectedBody, 0644) 77 | if err != nil { 78 | t.Fatalf("error creating index.html in temp directory: %s", err) 79 | } 80 | 81 | r := httptest.NewRequest("GET", "/", nil) 82 | w := httptest.NewRecorder() 83 | handler := http.FileServer(unindexed.Dir(dir)) 84 | handler.ServeHTTP(w, r) 85 | 86 | if w.Code != http.StatusOK { 87 | t.Fatalf("unexpected status received. expected %d got %d", http.StatusOK, w.Code) 88 | } 89 | 90 | resp := w.Result() 91 | defer resp.Body.Close() 92 | got, err := ioutil.ReadAll(resp.Body) 93 | if err != nil { 94 | t.Fatalf("error reading from response body: %s", err) 95 | } 96 | if !bytes.Equal(expectedBody, got) { 97 | t.Fatalf("unexpected response received. expected %s got %s", expectedBody, got) 98 | } 99 | } 100 | --------------------------------------------------------------------------------