Problem/Motivation
Drupal core doesn't provide any way to defend against file uploads attacks like the one described at https://soroush.secproject.com/blog/2014/05/even-uploading-a-jpg-file-ca...
This attack can most readily be mitigated by serving uploaded files from a different domain or subdomain where the user's session cannot be used to access private data from Drupal.
This issue reported via the Drupal 8 security bug bounty
https://tracker.bugcrowd.com/submissions/b54faf7e3c8a492dc289759fa17e0fc...
Proposed resolution
Provide a setting to override the domain used for public file URL or to replace part of it (e.g. to map to a different domain or subdomain)
This would be more effective if we stop sending session cookies to all subdomains per #2522002: Do not strip www. from cookie domain by default because that leaks session cookies to subdomains
Remaining tasks
patch
review
User interface changes
Additional section on the file system admin page (see screen shot)
API changes
Minor API addition
Data model changes
none

| Comment | File | Size | Author |
|---|---|---|---|
| #41 | provide_a_setting_to-2522008-41.patch | 6.52 KB | nlisgo |
| #36 | 2522008-32.patch | 6.49 KB | pwolanin |
| #32 | increment.txt | 1.37 KB | pwolanin |
| #32 | 2522008-32.patch | 6.49 KB | pwolanin |
| #29 | Screen Shot 2015-07-08 at 2.59.46 PM.png | 486.21 KB | pwolanin |
Comments
Comment #1
pwolanin commentedComment #2
effulgentsia commentedBecause this is a security improvement, I think it's ok to still do for 8.0, though if it doesn't get done in time, it can also go into 8.1.
I disagree with making it config. I think just a settings.php setting is enough. Just like
file_public_path. This means it can be shown in the UI, but not edited. See FileSystemForm::buildForm().Comment #3
effulgentsia commentedThe decision to use settings instead of config for
file_public_pathwas made in #1856766: Convert file_public_path to the new settings system (make it not configurable via UI or YAML). Not sure that the reasons there necessarily apply to whatever we name this new thing (file_public_host?), but I think the two variables should be in the same system (i.e., either both config or both settings, but not one of each).Comment #4
pwolanin commentedAn example of how such a setting might work at the stream wrapper level
Comment #5
pwolanin commented@effulgentsia - sure, it makes sense they would be in the same system. Would we need a commented-out example in default.settings.php?
Also, this proposes a preg_replace(), but a strtr might actually be more appropriate? e.g. the setting could just be:
Instead of like:
Comment #6
jplopezy commentedHi,
I have found this security issue.
Thanks Drupal for fix this issue.
regards
Comment #7
pwolanin commentedComment #8
dave reidWhy not just make this an alter hook instead? It feels like we're limiting ourselves with what can be done.
Comment #9
dave reidCouldn't this also be accomplished by using hook_file_url_alter(), especially since the example code in that hook is exactly this use case?
Comment #10
pwolanin commented@Dave Reid - so this is for the narrow use case of core mapping only public files to another domain. In other words, I'd like to be able to provide a doc on how to serve Drupal 8 user-uploaded files more securely without needing any contrib modules or custom code.
Yes, it overlaps with that hook, but generally it doesn't make sense for core to implement its own hooks, and the hook would be used e.g. by the CDN module for more complex use cases.
Here's a patch with a few further tweaks including showing the result in the UI + screenshot.
Comment #11
dave reidHrm, it just feels like we're investing a whole other thing that duplicates something already in core. The UI addition is good, but could also be made to work with the result of hook_file_url_alter().
Comment #12
pwolanin commentedThe form is actually a bit weird since it could possibly get an instance of what responds to public:// instead of calling those static methods, but maybe the goal is to show just what the core/default implementation will do.
To me, keeping this contained in the class makes it easier to replace via swapping the service, while hook_file_url_alter() is going to act on a lot more things.
Comment #13
effulgentsia commentedI think this is different than hook_file_url_alter(), because hook_file_url_alter() does something a little different than its name implies. hook_file_url_alter() is for altering the URI of a file prior to the URI being converted to a URL. So, for example, a CDN module could use the hook to alter
public://foo.txttomy-cdn://foo.txtand then let themy-cdnstream wrapper decide how to map that to an external URL (via getExternalUrl()). Of course,httpandhttpsURIs are also URLs, so hook_file_url_alter() can also be used to alterpublic://foo.txtdirectly tohttp://.../foo.txt, and you're right that the documented example implementation of hook_file_url_alter() does that, which perhaps isn't ideal.So I don't think the use-case here should be implemented via hook_file_url_alter(), because then we'd lose the information that it's still a public:// file. For example, suppose a CDN module ran at a higher weight, then our implementation would turn
public://foo.txttohttp://internal-not-cdn.subdomain/foo.txtand the CDN module wouldn't then recognize this as something that should be further mapped to a CDN. In other words, I think as much as possible, hook_file_url_alter() should keep $uri as a URI and defer conversion to URL to the stream wrapper.But, I think using a replacement pattern here is overkill. I think the setting should be an exact base URL, like
file_public_base_url, and if someone needs it to be dynamic, then they can implement the code to make it so. Note that it can be a protocol-relative base URL, and probably should be in the commented out example we put into default.settings.php.Comment #14
pwolanin commented@effulgentsia - to me the replacement is more useful since I want to be able to e.g. do different mappings in dev and prod, or map the "www" subdomain in both dev and prod to "downloads"
Comment #15
effulgentsia commentedYour settings.php needs to be different on dev and prod anyway, due to
$databases. And if you're smart enough to make $databases vary by environment via PHP code in the settings.php or its includes, then you can figure out how to do the same for$settings['file_public_base_url']. Same for multisite.Comment #16
effulgentsia commentedE.g., you can make
$settings['file_public_base_url']an object that implements a __toString() method if you want to make it dependent on $base_url; not sure if default.settings.php should mention that in a comment or not.Comment #17
pwolanin commentedDidn't mention it here, but included a string cast.
Comment #18
wim leers#13: the URI vs URL distinction is technically correct, but Drupal 7 didn't make that distinction. It was just "file URLs", period. The CDN module's
hook_file_url_alter()implementation does receive a file URI and then returns a file URL. It doesn't use stream wrappers.This makes a ton of sense. And is the best justification for #10, and answers the concerns in #11 by Dave Reid.
Code review
Overall, +1, thanks to the excellent rationale provided by @effulgentsia.
The mention of "Instance" here is very confusing/weird. What instance of what?
Nit: 80 cols.
Nit: s/sub-domain/subdomain/ (I think?)
Nit: Missing trailing period.
Comment #19
pwolanin commentedAll the lines are < 80 col afaict. Fixed other nits.
Comment #20
wim leersStill missing test coverage.
Comment #21
pwolanin commentedAh, sorry - I missed that suggestion.
Here's some test coverage. Also, realized there is little value to storing the values in the instance.
Comment #22
znerol commentedDo you think we can get rid of the configurable
$base_urlglobal insettings.phpwith this change?Comment #23
pwolanin commented@znerol - no, this is limited to public files and doesn't affect general URL generation.
Comment #24
znerol commentedFiled #2528988: Remove the option to specify a base_url from within settings.php.
Comment #25
wim leersTests look good :)
This means you're making it an API, but there's no interface. So if somebody provides alternative implementation of the
stream_wrapper.publicinterface, this will break.Either needs to not be public, or needs an interface.
s/host name/base URL/
(For consistency.)
Comment #26
pwolanin commentedFileSystemForm.php already uses the PublicStream class directly and calls a static method not on the interface, so I think the basic answer is that this admin page already doesn't give you accurate information if you replace the public stream wrapper.
Comment #27
effulgentsia commentedFor simple CDN integration, we might not want the directory path automatically appended. Should we make the return value in the first line just
baseUrl() . '/' . UrlHelper::encodePath($path), and require the setting, if specified, to includesites/default/filesif that's wanted?Comment #28
wim leers#27 is an excellent point.
Comment #29
pwolanin commentedOk, changed that and added some code comments suggested by Wim Leers + screen shot to show it working locally.
Comment #30
pwolanin commentedComment #32
pwolanin commentedforgot to fix the test to match.
Comment #33
dawehnerIsn't that already supported by all the magic in:
file_create_url()?Comment #34
effulgentsia commentedSee #13: this does it without needing to implement the alter hook and worry about how it plays with other implementations of the alter. Since it's a good security best practice, it's nice to support as a simple setting.
Comment #36
pwolanin commentedre-posting patch.
Comment #39
pwolanin commentedComment #40
nlisgo commentedComment #41
nlisgo commentedComment #44
wim leersAFAICT all feedback has been addressed.
Nit: Missing trailing period. Can be fixed on commit.
Comment #47
pwolanin commentedLooks like a temporary or sporadic fail that kicked it back.
Comment #48
effulgentsia commentedAdding credit to @jplopezy for reporting the original security issue and to @Wim Leers for substantive reviews.
Comment #49
effulgentsia commentedPushed to 8.0.x! Thanks for the nice security hardening.
Comment #53
tim.plunkettwtf, d.o?