Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Disallow access in publicly exposed services #1761

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 61 additions & 20 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"io/fs"
"io/ioutil"
"net"
"net/http"
"net/url"
"path"
Expand Down Expand Up @@ -37,13 +38,24 @@ var version string
var buildstamp string
var githash string

const loginEndPoint = "/login"

func allowUnauthenticated(r *http.Request) bool {
return strings.HasPrefix(r.URL.Path, "/login") || r.URL.Path == "/css"
return strings.HasPrefix(r.URL.Path, loginEndPoint) || r.URL.Path == "/css"
}

func authenticateHandler() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c := config.GetInstance()

if c.GetSecurityTripwireAccessedFromPublicInternet() {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Stash has been accessed from the public internet, and is not serving any more content to protect your privacy. " +
"More information is available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet"))
return
}

userID, err := manager.GetInstance().SessionStore.Authenticate(w, r)
if err != nil {
if err != session.ErrUnauthorized {
Expand All @@ -61,28 +73,59 @@ func authenticateHandler() func(http.Handler) http.Handler {
return
}

c := config.GetInstance()
ctx := r.Context()

// handle redirect if no user and user is required
if userID == "" && c.HasCredentials() && !allowUnauthenticated(r) {
// if we don't have a userID, then redirect
// if graphql was requested, we just return a forbidden error
if r.URL.Path == "/graphql" {
w.Header().Add("WWW-Authenticate", `FormBased`)
w.WriteHeader(http.StatusUnauthorized)
if c.HasCredentials() {
// authentication is required
if userID == "" && !allowUnauthenticated(r) {
//authentication was not received, redirect
// if graphql was requested, we just return a forbidden error
if r.URL.Path == "/graphql" {
w.Header().Add("WWW-Authenticate", `FormBased`)
w.WriteHeader(http.StatusUnauthorized)
return
}

// otherwise redirect to the login page
u := url.URL{
Path: "/login",
}
q := u.Query()
q.Set(returnURLParam, r.URL.Path)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
return
}

// otherwise redirect to the login page
u := url.URL{
Path: "/login",
} else {
//authentication is not required
//security fix: traffic from the public internet with no auth is disallowed
if !c.GetDangerousAllowPublicWithoutAuth() && !c.IsNewSystem() {
requestIPString := r.Header.Get("X-FORWARDED-FOR")
kermieisinthehouse marked this conversation as resolved.
Show resolved Hide resolved
if requestIPString == "" {
requestIPString = r.RemoteAddr[0:strings.LastIndex(r.RemoteAddr, ":")]
}
requestIP := net.ParseIP(requestIPString)
logger.Error(requestIP)

_, cgNatAddrSpace, _ := net.ParseCIDR("100.64.0.0/10")
if !(requestIP.IsPrivate() || cgNatAddrSpace.Contains(requestIP)) {
logger.Error("Stash has been accessed from the internet, without authentication. \n" +
"This is extremely dangerous! The whole world can see your stash page and browse your files! \n" +
"You probably forwarded a port from your router. At the very least, add a password to stash in the settings. \n" +
"Stash will not start again until you edit config.yml and change security_tripwire_accessed_from_public_internet to false. \n" +
"More information is available at https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet \n" +
"Stash is not answering any other requests to protect your privacy.")
c.Set(config.SecurityTripwireAccessedFromPublicInternet, true)
c.Write()
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("You have attempted to access Stash over the internet, and authentication is not enabled. " +
"This is extremely dangerous! The whole world can see your your stash page and browse your files! " +
"Stash is not answering any other requests to protect your privacy. " +
"Please read the log entry or visit https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet "))
manager.GetInstance().Shutdown()
return
}
}
q := u.Query()
q.Set(returnURLParam, r.URL.Path)
u.RawQuery = q.Encode()
http.Redirect(w, r, u.String(), http.StatusFound)
return
}

ctx = session.SetCurrentUserID(ctx, userID)
Expand All @@ -94,8 +137,6 @@ func authenticateHandler() func(http.Handler) http.Handler {
}
}

const loginEndPoint = "/login"

func Start(uiBox embed.FS, loginUIBox embed.FS) {
initialiseImages()

Expand Down
25 changes: 25 additions & 0 deletions pkg/manager/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,12 @@ const SlideshowDelay = "slideshow_delay"
const HandyKey = "handy_key"
const FunscriptOffset = "funscript_offset"

// Security
const dangerousAllowPublicWithoutAuth = "dangerous_allow_public_without_auth"
const dangerousAllowPublicWithoutAuthDefault = "false"
const SecurityTripwireAccessedFromPublicInternet = "security_tripwire_accessed_from_public_internet"
const securityTripwireAccessedFromPublicInternetDefault = "false"

// DLNA options
const DLNAServerName = "dlna.server_name"
const DLNADefaultEnabled = "dlna.default_enabled"
Expand Down Expand Up @@ -838,6 +844,22 @@ func (i *Instance) GetFunscriptOffset() int {
return viper.GetInt(FunscriptOffset)
}

// GetDangerousAllowPublicWithoutAuth determines if the security feature is enabled.
// See https://github.com/stashapp/stash/wiki/Authentication-Required-When-Accessing-Stash-From-the-Internet
func (i *Instance) GetDangerousAllowPublicWithoutAuth() bool {
i.RLock()
defer i.RUnlock()
return viper.GetBool(dangerousAllowPublicWithoutAuth)
}

// GetSecurityTripwireAccessedFromPublicInternet only true if stash has been accessed from the public internet,
// with no auth enabled, and DangerousAllowPublicWithoutAuth disabled
func (i *Instance) GetSecurityTripwireAccessedFromPublicInternet() bool {
i.RLock()
defer i.RUnlock()
return viper.GetBool(SecurityTripwireAccessedFromPublicInternet)
}

// GetDLNAServerName returns the visible name of the DLNA server. If empty,
// "stash" will be used.
func (i *Instance) GetDLNAServerName() string {
Expand Down Expand Up @@ -982,6 +1004,9 @@ func (i *Instance) setDefaultValues(write bool) error {

viper.SetDefault(Database, defaultDatabaseFilePath)

viper.SetDefault(dangerousAllowPublicWithoutAuth, dangerousAllowPublicWithoutAuthDefault)
viper.SetDefault(SecurityTripwireAccessedFromPublicInternet, securityTripwireAccessedFromPublicInternetDefault)

// Set generated to the metadata path for backwards compat
viper.SetDefault(Generated, viper.GetString(Metadata))

Expand Down