From 2b8f72b9daf6fe28cb4ae8f380fe9b94f4b893d3 Mon Sep 17 00:00:00 2001 From: Charles-Antoine Mathieu Date: Fri, 15 Jul 2016 14:40:36 +0200 Subject: [PATCH 1/4] Add security headers to getFileHandler to avoid HTML rendring in web browser (#162) --- server/handlers/getFile.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/server/handlers/getFile.go b/server/handlers/getFile.go index d3757902..a6c73a32 100644 --- a/server/handlers/getFile.go +++ b/server/handlers/getFile.go @@ -34,6 +34,7 @@ import ( "io" "net/http" "strconv" + "strings" "github.com/root-gg/plik/server/Godeps/_workspace/src/github.com/gorilla/mux" "github.com/root-gg/plik/server/Godeps/_workspace/src/github.com/root-gg/juliet" @@ -119,8 +120,24 @@ func GetFile(ctx *juliet.Context, resp http.ResponseWriter, req *http.Request) { } } + // Avoid rendering HTML in browser + if strings.Contains(file.Type, "html") { + file.Type = "text/plain" + } + + if file.Type == "" || strings.Contains(file.Type, "flash") { + file.Type = "application/octet-stream" + } + // Set content type and print file resp.Header().Set("Content-Type", file.Type) + + /* Additional security headers for possibly unsafe content */ + resp.Header().Set("X-Content-Type-Options", "nosniff") + resp.Header().Set("X-XSS-Protection", "1; mode=block") + resp.Header().Set("X-Frame-Options", "DENY") + resp.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'none'; style-src 'none'; img-src 'none'; connect-src 'none'; font-src 'none'; object-src 'none'; media-src 'none'; child-src 'none'; form-action 'none'; frame-ancestors 'none'; plugin-types ''; sandbox ''") + if file.CurrentSize > 0 { resp.Header().Set("Content-Length", strconv.Itoa(int(file.CurrentSize))) } From 5e4dbc61aacb5263d1ca9d91e3fb56a923a2b639 Mon Sep 17 00:00:00 2001 From: Charles-Antoine Mathieu Date: Fri, 15 Jul 2016 16:09:08 +0200 Subject: [PATCH 2/4] Enforce download domain option --- client/plik.go | 9 ++++++++- server/common/config.go | 12 ++++++++++++ server/common/upload.go | 14 ++++++++------ server/handlers/getFile.go | 12 ++++++++++++ server/plikd.cfg | 1 + server/public/js/app.js | 3 ++- 6 files changed, 43 insertions(+), 8 deletions(-) diff --git a/client/plik.go b/client/plik.go index 6d54477b..92df96ee 100644 --- a/client/plik.go +++ b/client/plik.go @@ -426,7 +426,14 @@ func getFileURL(upload *common.Upload, file *common.File) (fileURL string) { mode = "stream" } - fileURL += fmt.Sprintf("%s/%s/%s/%s/%s", config.Config.URL, mode, upload.ID, file.ID, file.Name) + var domain string + if upload.DownloadDomain != "" { + domain = upload.DownloadDomain + } else { + domain = config.Config.URL + } + + fileURL += fmt.Sprintf("%s/%s/%s/%s/%s", domain, mode, upload.ID, file.ID, file.Name) // Parse to get a nice escaped url u, err := url.Parse(fileURL) diff --git a/server/common/config.go b/server/common/config.go index 04d65298..f7796c96 100644 --- a/server/common/config.go +++ b/server/common/config.go @@ -31,6 +31,7 @@ package common import ( "net" + "net/url" "strings" "github.com/root-gg/plik/server/Godeps/_workspace/src/github.com/BurntSushi/toml" @@ -52,6 +53,9 @@ type Configuration struct { SslCert string `json:"-"` SslKey string `json:"-"` + DownloadDomain string `json:"downloadDomain"` + DownloadDomainURL *url.URL `json:"-"` + YubikeyEnabled bool `json:"yubikeyEnabled"` YubikeyAPIKey string `json:"-"` YubikeyAPISecret string `json:"-"` @@ -168,5 +172,13 @@ func LoadConfiguration(file string) { Config.Authentication = false } + if Config.DownloadDomain != "" { + strings.Trim(Config.DownloadDomain, "/ ") + var err error + if Config.DownloadDomainURL, err = url.Parse(Config.DownloadDomain); err != nil { + Logger().Fatalf("Invalid download domain URL %s : %s", Config.DownloadDomain, err) + } + } + Logger().Dump(logger.DEBUG, Config) } diff --git a/server/common/upload.go b/server/common/upload.go index e62e5cc5..23999bc3 100644 --- a/server/common/upload.go +++ b/server/common/upload.go @@ -41,12 +41,13 @@ var ( // Upload object type Upload struct { - ID string `json:"id" bson:"id"` - Creation int64 `json:"uploadDate" bson:"uploadDate"` - TTL int `json:"ttl" bson:"ttl"` - ShortURL string `json:"shortUrl" bson:"shortUrl"` - RemoteIP string `json:"uploadIp,omitempty" bson:"uploadIp"` - Comments string `json:"comments" bson:"comments"` + ID string `json:"id" bson:"id"` + Creation int64 `json:"uploadDate" bson:"uploadDate"` + TTL int `json:"ttl" bson:"ttl"` + ShortURL string `json:"shortUrl" bson:"shortUrl"` + DownloadDomain string `json:"downloadDomain" bson:"-"` + RemoteIP string `json:"uploadIp,omitempty" bson:"uploadIp"` + Comments string `json:"comments" bson:"comments"` Files map[string]*File `json:"files" bson:"files"` @@ -98,6 +99,7 @@ func (upload *Upload) Sanitize() { for _, file := range upload.Files { file.Sanitize() } + upload.DownloadDomain = Config.DownloadDomain } // GenerateRandomID generates a random string with specified length. diff --git a/server/handlers/getFile.go b/server/handlers/getFile.go index a6c73a32..84e86ae9 100644 --- a/server/handlers/getFile.go +++ b/server/handlers/getFile.go @@ -47,6 +47,18 @@ import ( func GetFile(ctx *juliet.Context, resp http.ResponseWriter, req *http.Request) { log := common.GetLogger(ctx) + // If a download domain is specified verify that the request comes from this specific domain + if common.Config.DownloadDomainURL != nil { + if req.Host != common.Config.DownloadDomainURL.Host { + downloadURL := fmt.Sprintf("%s://%s/%s", + common.Config.DownloadDomainURL.Scheme, + common.Config.DownloadDomainURL.Host, + req.RequestURI) + http.Redirect(resp, req, downloadURL, 301) + return + } + } + // Get upload from context upload := common.GetUpload(ctx) if upload == nil { diff --git a/server/plikd.cfg b/server/plikd.cfg index fec5013b..1d3d227e 100644 --- a/server/plikd.cfg +++ b/server/plikd.cfg @@ -18,6 +18,7 @@ MaxTTL = 2592000 # -1 => No limit SslEnabled = false SslCert = "" # Path to your certificate file SslKey = "" # Path to your certificate private key file +DownloadDomain = "" # Enforce download domain ( ex: https://dl.plik.root.gg ) YubikeyEnabled = false # Enable Yubikey Functionnality YubikeyAPIKey = "" # Yubikey API Key (get one on https://upgrade.yubico.com/getapikey/) diff --git a/server/public/js/app.js b/server/public/js/app.js index 11d7bc97..1dab38f7 100644 --- a/server/public/js/app.js +++ b/server/public/js/app.js @@ -662,7 +662,8 @@ function MainCtrl($scope, $api, $config, $route, $location, $dialog) { $scope.getFileUrl = function (file, dl) { if (!file || !file.metadata) return; var mode = $scope.upload.stream ? "stream" : "file"; - var url = location.origin + '/' + mode + '/' + $scope.upload.id + '/' + file.metadata.id + '/' + file.metadata.fileName; + var domain = $scope.config.downloadDomain ? $scope.config.downloadDomain : location.origin; + var url = domain + '/' + mode + '/' + $scope.upload.id + '/' + file.metadata.id + '/' + file.metadata.fileName; if (dl) { // Force file download url += "?dl=1"; From 04cc879c51fca91275f5639d2de61ffc833df1ed Mon Sep 17 00:00:00 2001 From: Charles-Antoine Mathieu Date: Fri, 15 Jul 2016 19:11:36 +0200 Subject: [PATCH 3/4] Add README security section --- README.md | 54 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 4f5f9f5e..11a76463 100644 --- a/README.md +++ b/README.md @@ -127,30 +127,6 @@ curl -s 'https://127.0.0.1:8080/file/0KfNj6eMb93ilCrl/q73tEBEqM04b22GP/mydirecto Client configuration and preferences are stored at ~/.plikrc ( overridable with PLIKRC environement variable ) -### Authentication - -Plik can authenticate users using Google and/or OVH API. -Once authenticated the only call Plik will ever make to those API is get the user ID, name and email. -Plik will never forward any upload data or metadata to any third party. -If source IP address restriction is enabled, user accounts can only be created from trusted IPs. But then -authenticated users can upload files without source IP restriction. - - - **Google** : - - You'll need to create a new application in the [Google Developper Console](https://console.developers.google.com) - - You'll be handed a Google API ClientID and a Google API ClientSecret that you'll need to put in the plikd.cfg file. - - Do not forget to whitelist valid origin and redirect url ( https://yourdomain/auth/google/callback ) for your domain. - - - **OVH** : - - You'll need to create a new application in the OVH API : https://eu.api.ovh.com/createApp/ - - You'll be handed an OVH application key and an OVH application secret key that you'll need to put in the plikd.cfg file. - -Once authenticated a user can generate upload tokens that can be specified in the ~/.plikrc file to authenticate -the command line client. - -``` -Token = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -``` - ### Available data backends Plik is shipped with multiple data backend for uploaded files and metadata backend for the upload metadata. @@ -195,6 +171,36 @@ Only suitable for a single instance deployment as the Bolt database can only be Suitable for distributed / High Availability deployment. +### Authentication + +Plik can authenticate users using Google and/or OVH API. +Once authenticated the only call Plik will ever make to those API is get the user ID, name and email. +Plik will never forward any upload data or metadata to any third party. +If source IP address restriction is enabled, user accounts can only be created from trusted IPs. But then +authenticated users can upload files without source IP restriction. + + - **Google** : + - You'll need to create a new application in the [Google Developper Console](https://console.developers.google.com) + - You'll be handed a Google API ClientID and a Google API ClientSecret that you'll need to put in the plikd.cfg file. + - Do not forget to whitelist valid origin and redirect url ( https://yourdomain/auth/google/callback ) for your domain. + + - **OVH** : + - You'll need to create a new application in the OVH API : https://eu.api.ovh.com/createApp/ + - You'll be handed an OVH application key and an OVH application secret key that you'll need to put in the plikd.cfg file. + +Once authenticated a user can generate upload tokens that can be specified in the ~/.plikrc file to authenticate +the command line client. + +``` +Token = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +``` + +### Security +Plik allow users to upload and serve any content as-is, but hosting untrusted HTML raises some well known security concerns. +Plik will try to avoid HTML rendering by overriding Content-Type to "text-plain" instead of "text/html". +Also the [Content-Security-Policy](https://content-security-policy.com/) HTTP header should disable sensible features of most recent browsers like resource loading, xhr requests, iframes,... +Along with that it is still strongly advised to serve uploaded files on a separate (sub-)domain to fight against phishing links and to protect Plik's session cookie with the DownloadDomain configuration parameter. + ### API Plik server expose a HTTP API to manage uploads and get files : From c87fdf664d5db8d5ffeb7fd30a6b72314c395640 Mon Sep 17 00:00:00 2001 From: Charles-Antoine Mathieu Date: Mon, 18 Jul 2016 11:38:46 +0200 Subject: [PATCH 4/4] Update go version in travis build --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 431baa81..d7609003 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go go: - - 1.5.2 + - 1.6.2 before_install: - npm install -g bower @@ -10,4 +10,4 @@ before_script: - go get -u github.com/golang/lint/golint script: - - make test && make \ No newline at end of file + - make test && make