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

Allow to password protect shares #1252

Merged
merged 1 commit into from Mar 2, 2021
Merged

Conversation

alvaroaleman
Copy link
Contributor

This changes allows to password protect shares. It works by:

  • Allowing to optionally pass a password when creating a share
  • If set, the password + salt that is configured via a new flag will be
    hashed via bcrypt and the hash stored together with the rest of the
    share
  • Additionally, a random 96 byte long token gets generated and stored
    as part of the share
  • When the backend retrieves an unauthenticated request for a share
    that has authentication configured, it will return a http 401
  • The frontend detects this and will show a login prompt
  • The actual download links are protected via an url arg that contains
    the previously generated token. This allows us to avoid buffering the
    download in the browser and allows pasting the link without breaking
    it

Thanks a lot for all the existing work on filebrowser btw, it's great!

Fixes #1223

Copy link
Contributor

@ramiresviana ramiresviana left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is not working correctly as the way it is, you need to do some testing. Also found some issues:

  • Unprotected shares cannot be accessed.
  • There's no feedback when user type password wrong.

http/public.go Outdated Show resolved Hide resolved
frontend/src/views/Share.vue Outdated Show resolved Hide resolved
frontend/src/components/prompts/Share.vue Outdated Show resolved Hide resolved
frontend/src/components/prompts/Share.vue Outdated Show resolved Hide resolved
@alvaroaleman
Copy link
Contributor Author

alvaroaleman commented Feb 2, 2021

This PR is not working correctly as the way it is, you need to do some testing

@ramiresviana can you elaborate on that, what exactly is not working correctly? Or do you just mean the two issues you mentioned?

@ramiresviana
Copy link
Contributor

This PR is not working correctly as the way it is, you need to do some testing

@ramiresviana can you elaborate on that, what exactly is not working correctly? Or do you just mean the two issues you mentioned?

The most important aspect of this feature was not working, the password validation. On my tests i was able to access any share by providing a wrong password. Also found another issue:

  • Downloading selected files, using the select button on the header bar, is not possible. Looks like the download url is missing the token.

@alvaroaleman
Copy link
Contributor Author

@ramiresviana addressed all feedback:

  • Passwords are now properly checked
  • Also added some tests for that
  • Shares that are created without password now end up actually not having a password
  • Entering a wrong password will result in an error being shown
  • Selecting files and then downloading them via the button at the top works
  • No inline styling
  • Localized placeholder

Please have another look.

Copy link
Contributor

@ramiresviana ramiresviana left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using JWT based authentication? I think would be a more secure approach.

frontend/src/views/Share.vue Outdated Show resolved Hide resolved
frontend/src/views/Share.vue Show resolved Hide resolved
frontend/src/views/Share.vue Outdated Show resolved Hide resolved
frontend/src/views/Share.vue Outdated Show resolved Hide resolved
frontend/src/components/prompts/Share.vue Outdated Show resolved Hide resolved
@alvaroaleman
Copy link
Contributor Author

What about using JWT based authentication? I think would be a more secure approach.

First, if you have conceptional remarks about a change, it would be awesome to give those in the first round of feedback, not in the second one. Second, can you elaborate on how exactly JWT would be more secure? The password is sent exactly once, that will be needed however auth works. The token can be any random value which must be hard to guess but making it a JWT or not will not make anything more or less secure.

@ramiresviana
Copy link
Contributor

ramiresviana commented Feb 8, 2021

What about using JWT based authentication? I think would be a more secure approach.

First, if you have conceptional remarks about a change, it would be awesome to give those in the first round of feedback, not in the second one. Second, can you elaborate on how exactly JWT would be more secure? The password is sent exactly once, that will be needed however auth works. The token can be any random value which must be hard to guess but making it a JWT or not will not make anything more or less secure.

Just wanted to hear your opinion about JWT, that's not an requirement. I was worried about the fact that the share token is always the same, with the JWT standard the token needs to be refreshed periodically and the frontend do not have access to the payload (it has access to payload but cannot generate a valid token).

@alvaroaleman
Copy link
Contributor Author

Just wanted to hear your opinion about JWT, that's not an requirement. I was worried about the fact that the share token is always the same, with the JWT standard the token needs to be refreshed periodically and the frontend do not have access to the payload (it has access to payload but cannot generate a valid token).

Well, I guess that depends on the expectation. Right now the download links from the share will work indefinitely for as long as that share exists, which is nice for example if you want to be able to pass around a link that will work without making the share public. If I wanted to brute force a filebrowser share, I would most likely try to brute force the password instead, because I would assume that it is almost always shorter than the 96 byte of the token and that it will in most cases be easier to guess.

@alvaroaleman
Copy link
Contributor Author

@o1egl Can you have a look, please?

@@ -2,7 +2,7 @@ version: 2
jobs:
lint:
docker:
- image: golangci/golangci-lint:v1.27.0
- image: golangci/golangci-lint:v1.31.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last version is v1.36.0. Either upgrade to the latest or revert it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not look like that, but the version was a very conscious choice, it had to be updated to something with go 1.15 to have support for *testing.T.TempDir which was added in go 1.15. This is the oldest version of the golangci-lint that uses go 1.15.

Updating it to v1.36.0 results in a bunch of new failures, because filebrowser doesn't have a golangci-lint config, so it uses whatever its default linters are, which is not stable across versions.

cmd/root.go Outdated Show resolved Hide resolved
frontend/src/api/files.js Outdated Show resolved Hide resolved
Comment on lines +81 to +82
password: '',
passwordPermalink: ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it 2 different variables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because its two different inputs

frontend/src/i18n/en.json Outdated Show resolved Hide resolved
http/public_test.go Outdated Show resolved Hide resolved
http/share.go Outdated Show resolved Hide resolved
http/share.go Outdated Show resolved Hide resolved
share/storage.go Outdated
Comment on lines 105 to 107
if len(links) > i+1 {
links = append(links[:i], links[i+1:]...)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change is introduced?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because a bunch of methods in storage.go unconditionally access links[i+1:] which results in an out of bounds panic if the last link expired. I will remove it from this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check doesn't save from data race

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, what data race do you mean? It stops accessing an array in an unsafe manner and risking an out of bounds

users/storage.go Outdated
@@ -17,6 +17,15 @@ type StorageBackend interface {
DeleteByUsername(string) error
}

type Interface interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please give a proper name to this interface. UserStore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter will refuse that due to stuttering, package symbols should not start with the package name. This ends up being used as users.Interface which IMHO is totally reasonable

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just gave an example. It can be named Store. An interface name should be meaningful and self explainable. Interface name is too generic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed it to Store, please take another look

Copy link
Contributor Author

@alvaroaleman alvaroaleman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@o1egl all feedback addressed, ptal

@@ -2,7 +2,7 @@ version: 2
jobs:
lint:
docker:
- image: golangci/golangci-lint:v1.27.0
- image: golangci/golangci-lint:v1.31.0
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might not look like that, but the version was a very conscious choice, it had to be updated to something with go 1.15 to have support for *testing.T.TempDir which was added in go 1.15. This is the oldest version of the golangci-lint that uses go 1.15.

Updating it to v1.36.0 results in a bunch of new failures, because filebrowser doesn't have a golangci-lint config, so it uses whatever its default linters are, which is not stable across versions.

Comment on lines +81 to +82
password: '',
passwordPermalink: ''
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because its two different inputs

http/public.go Outdated
@@ -94,3 +104,26 @@ var publicDlHandler = withHashFile(func(w http.ResponseWriter, r *http.Request,

return rawDirHandler(w, r, d, file)
})

func authenticateShareRequest(r *http.Request, l *share.Link, salt string) (int, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An error here means something went wrong with our code and yes, in that case the status code can be ignored. Statuscode != 0 means the verification failed. The distinction is important, because they result in different http codes. Our code erroring is a 5XX, the verification failing is a 4XX, if we return an error in both cases, we can not distinguish those two anymore

share/storage.go Outdated
Comment on lines 105 to 107
if len(links) > i+1 {
links = append(links[:i], links[i+1:]...)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because a bunch of methods in storage.go unconditionally access links[i+1:] which results in an out of bounds panic if the last link expired. I will remove it from this PR.

users/storage.go Outdated
@@ -17,6 +17,15 @@ type StorageBackend interface {
DeleteByUsername(string) error
}

type Interface interface {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter will refuse that due to stuttering, package symbols should not start with the package name. This ends up being used as users.Interface which IMHO is totally reasonable

@alvaroaleman
Copy link
Contributor Author

@o1egl any chance you can have another look?

@o1egl
Copy link
Member

o1egl commented Mar 1, 2021

@alvaroaleman Get Permanent Link returns 400 error. It tries to decode an empty body

url = removePrefix(url)
url = `/api/share${url}`
if (expires !== '') {
url += `?expires=${expires}&unit=${unit}`
}

let body = '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server expects to receive a JSON. An empty string is not a correct JSON.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@o1egl fixed

This changes allows to password protect shares. It works by:
* Allowing to optionally pass a password when creating a share
* If set, the password + salt that is configured via a new flag will be
  hashed via bcrypt and the hash stored together with the rest of the
  share
* Additionally, a random 96 byte long token gets generated and stored
  as part of the share
* When the backend retrieves an unauthenticated request for a share that
  has authentication configured, it will return a http 401
* The frontend detects this and will show a login prompt
* The actual download links are protected via an url arg that contains
  the previously generated token. This allows us to avoid buffering the
  download in the browser and allows pasting the link without breaking
  it
@o1egl o1egl merged commit d8f415f into filebrowser:master Mar 2, 2021
Alsan pushed a commit to Alsan/filebrowser that referenced this pull request Aug 25, 2021
This changes allows to password protect shares. It works by:
* Allowing to optionally pass a password when creating a share
* If set, the password + salt that is configured via a new flag will be
  hashed via bcrypt and the hash stored together with the rest of the
  share
* Additionally, a random 96 byte long token gets generated and stored
  as part of the share
* When the backend retrieves an unauthenticated request for a share that
  has authentication configured, it will return a http 401
* The frontend detects this and will show a login prompt
* The actual download links are protected via an url arg that contains
  the previously generated token. This allows us to avoid buffering the
  download in the browser and allows pasting the link without breaking
  it
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants