Currently, if you set the path for the private file system to be the same as the public file system, the .htaccess file written is the one for the public stream wrapper. This means that any uploaded private files are publicly accessible. We should be validating that the two paths are at least different strings, or perhaps validating at the stream url level.
Update:
The patch in #31-46 does result in form_set_error() being called from within a submit() chain, which is less than ideal ... but this trade-off is balanced against the form validate() function attempting to create the directory within the filesystem (which is how the initial patch was handling things). As stated in #31, the current approach seems to be the lesser of two evils.
| Comment | File | Size | Author |
|---|---|---|---|
| #53 | validate-secure-directories-53.patch | 7.94 KB | jthorson |
| #53 | interdiff_46-53.txt | 1.34 KB | jthorson |
| #49 | validate-secure-directories-46.patch | 7.93 KB | jthorson |
| #46 | validate-secure-directories-46.patch | 7.93 KB | jthorson |
| #46 | interdiff_44-46.txt | 4.52 KB | jthorson |
Comments
Comment #1
chx commentedI disagree. There are a ton of horribly stupid things you can do and yet we do not verify all of them. One might even move this to http://drupal.org/project/security_review issue queue, honestly.
Comment #2
deviantintegral commentedHere is a patch in the meantime until it's decided if this is 7.x or 8.x.
Comment #3
webchickWhy on earth is adding validation here so that your privacy on private files isn't completely (and silently) destroyed a "feature request"?
Comment #4
webchickAnd I guess the sec. team should make the call on whether this is actually critical or not, but it sure seems like it to me.
Steps to reproduce:
1. Enter a private file directory at admin/config/media/file-system or whatever it is to, for example, sites/default/private-files.
2. Change article's image field to "private" file handling.
3. Create a new article with an image.
4. Note that you're denied access if you replace system/files with 'sites/default/private-files'. (Everything works as it should thanks to .htaccess)
5. Now, switch the path of the private files to sites/default/files (same as public). Drupal lets you do this without complaint.
6. Create another article with an image.
7. Note that your image is completely and utterly visible when you repeat step 4. Your security is gone.
Andrew has a hunch that this happens because "pu" comes after "pr" alphabetically, so the .htaccess file that's written is the public files one, not the private files one. One would expect the steps outlined here to break public file handling, not private handling, but in fact it does the opposite.
But in any case, it appears they're mutually exclusive and err on the side of "allow everything," so IMO we need to add validation to prevent users from doing this.
Comment #5
webchickAnd, there's a patch.
Comment #6
chx commentedWhy are you validating in after_build instead of validate?? what happens to those who upgrade? Why do we check against this stupid configuration and not when someone enables say <embed> for anonymous in filter configuration? What's the difference...? What makes this one so special?
Comment #7
deviantintegral commented#after_build was from when I initially was doing validation per-element. The attached patch changes it to be #validate.
In terms of what's unique about this issue, I think it comes down to simplicity. With input filters, an admin could allow embed in one filter, but strip it out later in another filter, and there's no way for core to easily check that. Is there ever a possibility that private / temporary files are the same path as the public directory, and still be secure? If not, I think this should be fixed.
Comment #8
RoboPhred commentedIf we shouldn't check this, why do we check for a valid path when adding menu items, or for duplicate path aliases? These are also cases where its not a "bug" in that drupal will run fine with the invalid data, yet its considered important to prevent inconsistent behavior with the site.
While <embed> is a security issue, its still a valid state; Drupal will function according to its own design and documentation, even if the end result is not what the user wanted. I agree that we should not stop a user from configuring the site to be less secure, when doing so follows the design of the configurable.
The key difference is that Drupal specifically defines the intended behavior and setup of the two paths:
Public: "This directory must be ... accessible over the web."
Private: "This directory should not be accessible over the web."
The documentation states a mutually incompatible attribute for each field, and so naturally its an invalid state if both fields share the same value. In addition, when both fields are identical, private files are no longer private, breaking its design.
Validating against this case just makes sense. Every other forum validates itself when necessary to prevent issues like this, so it's natural that these should two.
The case of a user setting the private file path to a public directory would not be a bug. However, a good feature would be a good idea to warn them of this.
Comment #9
Volx commentedI don't understand why the file system settings form uses #after_build at all. This method is called whenever the form is build, so whenever the form is build the system checks whether the directory is protected by a .htaccess file or not. I think that is a pretty random check and thus does not add any security. Moreover the directory and the .htaccess file are build when they don't exist on loading of a form, rather unexpected behavior in my point of view.
The patch above does not work as intended, since the validation comes after the .htaccess files have been created or at least independently of it, so if a private .htaccess will be overwritten it happens if the form validates or not. So since the call to system_check_directory in #after_build is not adding to security, we can remove it and move all the logic of creating and securing the directory to a submit function and keep the validate function from the patch above to take care of validation. I'll write a patch tomorrow if no one beats me to it :)
Comment #10
Volx commentedHere is the patch. I also changed the signature of system_check_directory by adding the optional parameter $private, that is passed into file_create_htaccess to make the function more generic and remove the hard-coded check for the name of the form element file_public_path.
Comment #11
roy smith commentedFWIW, this seems like a critical issue to me. I run a site where we upload both private and public documents. Accidentally allowing the private docs to be publicly seen would be a disaster. I do "horribly stupid things" all the time. It's part of being human. If there's a reasonable way software can prevent me from doing some of them, it should.
Comment #12
webchickI think we need to do this without the function signature change, if possible. That sounds like good polish, but not something we should introduce just before RC1.
Comment #13
deviantintegral commentedComment #14
deviantintegral commentedHere is a patch that:
However, I'm going to take a stab at getting rid of the $private flag to address #12.
Comment #15
deviantintegral commentedHere's a patch that removes the function signature change.
Comment #16
attiks commentedPatch works, one question: can private == tmp? doesn't really makes sense but maybe check for it as well?
Comment #17
deviantintegral commentedIf the private and temporary directories are the same, the files are still secure. Though it's not really a good practice, I can't see any security issues occurring because of it.
Comment #18
chx commentedPatch is looking good but...
Comment #19
deviantintegral commentedHere's an update, same as #15 but with tests.
Comment #20
deviantintegral commentedHmm. Testbot on QA says this passed, but the issue hasn't been updated here.
Comment #21
pfrenssenLooks good to me.
Comment #22
lars toomre commentedDoes it make sense to add the $private parameter present in #14 since this is going into D8 first? I liked that inclusion and think it got striped out because of the D7 API freeze.
Comment #23
sunMoving this logic into a submit handler is utterly wrong, since http://api.drupal.org/api/drupal/modules--system--system.module/function... throws form validation errors. system_check_directory() has to be invoked before (like #after_build) or within form validation.
Powered by Dreditor.
Comment #24
deviantintegral commentedIt's not clear to me how this would be done in a validate handler. Some of the errors thrown by system_check_directory() are the result of trying to create a directory or change permissions. Is it really better for a #validate handler to be modifying the system? It's a similar situation with #after_build, as just viewing the form could cause directories to be created or permissions changed.
At the least, here's a reroll of #19, but restores the $private parameter now that we're on Drupal 8.
Comment #26
tamerzg commentedLast patch failed due to missing group index in getInfo().
Comment #28
andyceo commented#26: validate-secure-directories.patch queued for re-testing.
Comment #30
star-szrTagging for reroll.
Comment #31
jthorson commentedRe-rolled. Updated #26, and removed the extra
if($form_element['#name'] == 'file_public_path'check that was still hanging around, but as far as I can tell, was no longer necessary with the introduction of the $private parameter.I tend to agree with sun's argument in #23, in that it feels wrong to be calling form_set_error() within system_check_directory() from a submit function ... but in the end, I felt more strongly about the argument in #24, that we shouldn't be modifying the file structure within the #validate function.
So unless we were to somehow solve the 'drupal_mkdir() as a validation check' issue, the approach in this patch feels like the lesser of two evils ...
Comment #32
star-szrThanks @jthorson!
Comment #33
yesct commentedThis has min docs gate trouble. http://drupal.org/core-gates#documentation-block-requirements
These need one line < 80 char summaries and typed params like:
* @param array $form
* An associative array containing the structure of the form.
* @param array $form_state
* A reference to a keyed array containing the current state of the form.
I think this need a type: boolean and (optional)
Comment #34
chx commentedmajor, meh!
Comment #35
yesct commentedYeah, I guess someone from security team can change it back to a higher priority. The earlier discussion about priority was from 2010.
Comment #36
jthorson commented#31: validate-secure-directories-31-test-only.patch queued for re-testing.
Comment #37
jthorson commented#31: validate-secure-directories-31.patch queued for re-testing.
Comment #38
jthorson commentedUpdated docblocks as per #1354: [Obsolete] API documentation and comment standards.
Comment #39
jthorson commentedMore docblock tweaks and formatting consistency.
Comment #40
yesct commentedSo.. this is not a _form() form constructor. Is that because this is a special .. something?
consider putting @ingroup forms after the @see's
Maybe more like
/**
* Form validation handler for system_file_system_settings forms.
*
* @see system_file_system_settings_form_submit()
*/
function system_file_system_settings_form_validate($form, &$form_state) {
...
}
/**
* Form submission handler for system_file_system_settings forms.
*
* Verify public, private, and temporary paths exist, or can be created. (or not here, and inline in the function)
*
* @see system_file_system_settings_form_validate()
*/
function system_file_system_settings_form_submit($form, &$form_state) {
No dot in the @see, and keywords in comment like Form validation handler, etc
Should the function names be ..._form_validate() and _form_submit()
from http://drupal.org/node/1354#forms
[check the names of functions, and names in the comments... my cut and paste is not exact.]
Comment #41
yesct commented#40 is a review of #38
for #39 ... just make your new stuff or the lines you changed match the standard.
Don't fix the whole file. Instead, make a follow-up to do that later.
Comment #42
jthorson commentedOoops ... lost the test file on the last few posts!
Docs updated as per #40, and added the test file back in.
Comment #43
yesct commentedMaking the one line that matches the pattern in 1354 is good.
But let's keep the comment that adds information about what it does specifically.
Add the extra lines like you did for the _submit()
(Or move the comments inline into the validate().)
The rest of the changes look really good!
The bot said green. :)
Is it worth doing a tests only version of this to show the bot fails?
Comment #44
jthorson commentedAdded back the description to the system_file_system_settings() form.
The test-only patch is posted in #31, and the interdiff in #42 shows that only docs have changed since then.
Comment #45
lars toomre commentedAttached are some nit-picky type of comments from reading through this patch.
This should be 'Contains' instead of 'Definition of'.
My understanding is that there should be a blank line after a class declartion and before the closing brace.
A class member should be 'adminUser' instead of 'admin_user' (camelCase).
Missing docblock for what this is testing.
Throughout patch... Assertion messages should not be translated with t(). These recently have been removed from most tests in D8.
system_file_system_settings() [Needs ()].
The type hint should be 'bool' instead of 'boolean'. Also please add type hint to all other @param/@return directives in any docblock changed.
Finally, this @param directive needs to explicitly state what the default value is (ie 'Defaults to TRUE.').
Comment #46
jthorson commentedUpdated with changes from #45.
Comment #47
lars toomre commentedThanks @jthorson. The changes look good.
Comment #49
jthorson commentedWow ... those look odd and unrelated!
Re-uploading to re-test (and still save the previous results incase we've got a random failure somewhere).
Comment #50
yesct commentedtriggering the testbot
Comment #51
webchickI'm not sure why this got demoted to "minor" ("meh" is not very descriptive :P) but publicly accessible private files does not in any way sound minor to me.
Comment #51.0
jthorson commentedUpdated issue summary.
Comment #52
attiks commentedCode looks good, bot is happy, only some minor comment issues.
Tests the setting ...
Tests ...
I think there should be a blank line above this.
why directory, why not "temporary file system path", so it's similar to public and private one?
Comment #53
jthorson commentedUpdated "Test" -> "Tests", and added the blank line before the @ingroup statement.
The "temporary directory" wording matches the actual label that is used on the field within the UI ... so that it is obvious to the user which field the error message is referencing.
Comment #54
chx commentedSeems like people are not heeding my warnings.
What happens to those who upgrade? Why do we check against this stupid configuration and not when someone enables say <embed> for anonymous in filter configuration? What's the difference...? What makes this one so special?
None of this was addressed. It is hardly an answer "we can check for this but we can't for filter so let's do this!". I could list N other stupid ways to configure your site. Say, public files go under files/public and private go under files/private just to stay close to this issue. There is no point, absolutely none in staking out one single security fail and "fixing" it. This would lead to people think that "oh I tried to do it, Drupal warned me it was unsafe, I changed it a little, now it doesn't, I am surely good!" and also "Drupal warned me there it doesn't warn me here, I am surely good!". Do not create unreasonable expectations.
And the question for those who upgrade went completely unaddressed. I have no idea what so ever how to fix that.
Comment #55
chx commentedRe #51 "publicly accessible private files does not in any way sound minor to me."
Not to me either but this patch does not even attempt to address that.
Comment #56
chx commentedFor example, if the patch would write a random size image into the private directory, try to <img src> it (styled margin-left: -5000px to be invisible) and checking dimensions via JS, that would be an attempt worthy of discussion to validate whether private files are accessible via public. We would still need to address of how not to create a false sense of security and how to deal with upgrade.
Comment #56.0
chx commentedUpdated issue summary.