Problem/Motivation

.htccess is supposed to prevent executing php files like those in vendor

  # Deny access to any other PHP files that do not match the rules above.
  # Specifically, disallow autoload.php from being served directly.
  RewriteRule "^(.+/.*|autoload)\.php$" - [F]

the .htaccess rule is somehow wrong or incomplete that supposed to prevent execution of php files in the vendor dir, however

/core/vendor/behat/mink/driver-testsuite/web-fixtures/headers.php/1

is executed while

/core/vendor/behat/mink/driver-testsuite/web-fixtures/headers.php

is a 403

Proposed resolution

Fix .htaccess in this issue and move vendor out of webroot in: #2508591: vendor/ is web accessible

Remaining tasks

User interface changes

n/a

API changes

n/a

Data model changes

n/a

originally reported as part of the Drupal 8 bug bounty
https://tracker.bugcrowd.com/submissions/77918e9ef18b0f36f4279efbdb39bf8...

original reporter: DongIT

Comments

tim.plunkett’s picture

Issue tags: -core .htaccess file rule to block PHP file execution can be bypassed +Security
dawehner’s picture

Does our web.config has the same issues?

dokumori’s picture

benjy’s picture

This rule only exists in D8 so I think this can be public. Re-publishing.

alexpott’s picture

Status: Active » Needs review
StatusFileSize
new1.12 KB
new1.72 KB

Here's a patch that prevents access to /core/vendor/behat/mink/driver-testsuite/web-fixtures/headers.php/1

The last submitted patch, 5: 2508666.5.test-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 5: 2508666.5.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new1.12 KB
new1.72 KB

Also this has nothing to to with vendor specifically... core/lib/Drupal.php/1 is just as accessible as /core/vendor/behat/mink/driver-testsuite/web-fixtures/headers.php/1.

Trying some new patches. Not sure what I did wrong.

The last submitted patch, 8: 2508666.8.test-only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 8: 2508666.8.patch, failed testing.

alexpott’s picture

Version: 8.1.x-dev » 8.0.x-dev

alexpott queued 8: 2508666.8.test-only.patch for re-testing.

pwolanin’s picture

Title: Drupal 8 .htaccess rule to prevent php file execution can be easily bypassed » Drupal 8 .htaccess rule to prevent php file access can be easily bypassed

Clarifying title - this is unrelated to e.g. the prevention of php execution in the files directories.

Status: Needs work » Needs review

alexpott queued 8: 2508666.8.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 8: 2508666.8.patch, failed testing.

The last submitted patch, 8: 2508666.8.test-only.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new1.36 KB
new2.49 KB

Fixing Simpletest's http.php and https.php

The last submitted patch, 17: 2508666.17.test-only.patch, failed testing.

dawehner’s picture

What happens with web.config just curious here ...

alexpott’s picture

@dawehner who as an IIS server kicking about?

dawehner’s picture

No idea. Maybe some of the commerce people, as they have developed some mssql driver.

cilefen’s picture

Would query strings break the protection without this patch?

cilefen’s picture

^^ no

benjy’s picture

Patch looks good, I tested this manually with as many variations as I could based on the initial issue report and couldn't reproduce after applying #17. +1 for RTBC.

alexpott’s picture

I'm not sure that we should be using the rewrite directive to deny access to php files. This change will prevent access to a path alias like "htttp://drupal.org/what.php/5.6-is-best". mod_rewrite is not matching on files it is matching on the requested url. It does not care if the underlying file exists or not - that's what the FileMatch directive is for.

benjy’s picture

StatusFileSize
new741 bytes

Here's an approach with FilesMatch, see what the bot thinks.

alexpott’s picture

StatusFileSize
new3.73 KB

I think this approach is slightly clearer and easier to grok.

I definitely agree the rewrite approach should be ditched. This basically gives us a list of possible front controller names that is simple to understand and alter.

alexpott’s picture

StatusFileSize
new421 bytes
new3.71 KB

A slight refinement.

alexpott’s picture

StatusFileSize
new1.56 KB
new4.33 KB

We can go further!

The last submitted patch, 26: 2508666-26.patch, failed testing.

alexpott’s picture

StatusFileSize
new4.03 KB
new406 bytes

A change for my local env crept in.

The last submitted patch, 27: 2508666-2.26.patch, failed testing.

alexpott’s picture

StatusFileSize
new403 bytes
new4.04 KB

Missed update.php

The last submitted patch, 28: 2508666-2.28.patch, failed testing.

The last submitted patch, 29: 2508666-2.29.patch, failed testing.

The last submitted patch, 31: 2508666-2.30.patch, failed testing.

benjy’s picture

#33 is awesome, much simpler to read and using FilesMatch makes sense. +1 from me.

dawehner’s picture

+++ b/.htaccess
@@ -132,22 +143,6 @@ AddEncoding gzip svgz
-  # Allow access to Statistics module's custom front controller.
-  # Copy and adapt this rule to directly execute PHP files in contributed or
-  # custom modules or to run another PHP application in the same directory.

At least this documentation is certainly kinda lost ...

alexpott’s picture

StatusFileSize
new4.6 KB
new1.24 KB

@dawehner good point.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Cool

pwolanin’s picture

Status: Reviewed & tested by the community » Needs work

So, I am not sure this is all working as desired. update.php is not a real file now, but removing it from the list blocks access.

Similarly, if I alias a node path to /zzz.php, I get a 403. Also /zzz.install. however, /index.php/zzz.install loads the page

I think this is why it was in the rewrite section - so that the rewrite to index.php would bypass it.

pwolanin’s picture

Status: Needs work » Needs review
StatusFileSize
new614 bytes

Here's a possible alternate fix for the problem - only act when the file exists on disk, and match php in the name.

I haven't tried all possibilities, but does seem to block use of a trailing slash to bypass the rule, while allowing a node aliased to /a/zzz.php

pwolanin’s picture

StatusFileSize
new581 bytes

Maybe can make it simpler. We already check in the index.php rewrite the !-f condition, so we shouldn't need to check the opposite.

How about just this?

pwolanin’s picture

Issue tags: +Needs tests

The last submitted patch, 42: 2508666-42.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 43: 2508666-43.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
StatusFileSize
new3.38 KB
new3.75 KB

Added tests for more files and anode aliased to test.php and test.php/test.

This looks like it might actually work!

@pwolanin++

mlhess’s picture

I can confirm this works for htaccess. Can we build in a status check for people who may not have htaccess setup correctly. Something that says their site is insecure, like the update notices?

The last submitted patch, 42: 2508666-42.patch, failed testing.

The last submitted patch, 43: 2508666-43.patch, failed testing.

pwolanin’s picture

@mlhess - reliably checking Drupal by trying to make http requests to itself has problems. e.g. see: #965078: HTTP request checking is unreliable and should be removed in favor of watchdog() calls

At this point I wonder if implementing some of these checks client site (i.e. JS) would be more sensible, since clearly the browser can access the site at the point you are viewing a report page.

chx’s picture

> Can we build in a status check for people who may not have htaccess setup correctly. Something that says their site is insecure, like the update notices?

If you do, you need to do it with JS and/or PHP-masquerading-as-image but server-to-server requests have been attempted and failed. There are too many variations to make it work reliably. So please do not try it again :)

alexpott’s picture

StatusFileSize
new3.81 KB

Rerolled.

I've looked at trying to do @mlhess's suggestion in #48 in light of #51 and #52. It's going to be quite problematic to add a requirement in system_requirement that only appears when rendering through a browser. hook_requirements() is supposedly for CLI as well. I think the best way forward is to open a followup issue for discussing the best way to do this. Which I've created - #2510194: Use JS or PHP-masquerading-as-image to test .htaccess on admin/reports/status

pwolanin’s picture

So, I think this might block access for a harmless text file named foo.php-info.txt? That was my motivation for including the (/|$) in my last patch.

pwolanin’s picture

Status: Needs review » Needs work

Indeed - confirmed that problem locally.

alexpott’s picture

Status: Needs work » Needs review
StatusFileSize
new7.41 KB
new8.75 KB

@pwolanin - you're right going with ($|/) - I was trying to reduce complexity :). I've also added test coverage to ensure that a file named foo.php-info.txt is not blocked.

And I've also completed test coverage for the FilesMatch directive in .htaccess and found a bug :)

<FilesMatch "\.(engine|inc|install|make|module|profile|po|sh|.*sql|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\..*|Entries.*|Repository|Root|Tag|Template)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$">

Is the current directive in D8 HEAD (and D7). This blocks access_test.module.save and access_test.module.orig but (mistakenly) permits both access_test.php.save and access_test.php.orig - it does however block access_test.php.orig.save (lol).

Care needs to be taken if this patch needs rerolls since our git is configured to ignore .swp , ~, and .orig files. You have to add them with a -f.

As @pwolanin says:

if it's not tested it doesn't work

alexpott’s picture

FYI: The bug wrt to .php.orig.save was added by #1907704: Restrict temporary files created by text editors

pwolanin’s picture

I think this code comment needs a fix - maybe remove the "If"?

+   * @return int[]
+   *   An array keyed by file paths. If the value is the expected response code,
+   *   for example, 200 or 403.
pwolanin’s picture

StatusFileSize
new8.75 KB
new657 bytes

Just fixing the comment

pwolanin’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +Needs backport to D7

Think this is good now. We should backport some of the fixes and tests to D7 also.

xjm’s picture

Status: Reviewed & tested by the community » Needs work

This is, like, adding emacs backups or something to core?

Edit: never mind.

xjm’s picture

Status: Needs work » Reviewed & tested by the community

Disregard #62. That is an intended part of the test.

  • catch committed acf9193 on 8.0.x
    Issue #2508666 by alexpott, pwolanin, benjy: Drupal 8 .htaccess rule to...
catch’s picture

Version: 8.0.x-dev » 7.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)

One comment typo, fixed on commit.

+++ b/core/modules/system/src/Tests/System/HtaccessTest.php
@@ -54,21 +93,52 @@ protected function getProtectedFiles() {
+    // Test that is it possible to have path aliases containing in .php.

s/in //

Committed/pushed to 8.0.x, thanks!

Moving to 7.x for backport of the relevant bits.

catch’s picture

Priority: Critical » Major

Major for 7.x - we don't have .php file protection there at all, so it's mainly firming up the .orig etc. blocking which is hardening only.

star-szr’s picture

In that typo comment referenced above shouldn't we also s/is it/it is/? Sorry to bring it up.

catch’s picture

@Cottser, yes, yes we should.

Also https://www.youtube.com/watch?v=YIyDXCYe7lM

pwolanin’s picture

Title: Drupal 8 .htaccess rule to prevent php file access can be easily bypassed » Drupal 7 .htaccess rule to prevent backup file access is broken and needs more tests
pwolanin’s picture

DongIT’s picture

I found this bug

alexpott’s picture

Ticking the @DongIT checkbox in the credit list.

Jorgee’s picture

Does this affect Nginx as well?

alexpott’s picture

@Jorgee nginx has the same security considerations as apache does wrt to access to .php files. Drupal does not supply nginx configuration scripts. But a good place to start to see if the nginx configuration you are using has the expected security provisions is to run HtaccessTest.

  • catch committed acf9193 on 8.1.x
    Issue #2508666 by alexpott, pwolanin, benjy: Drupal 8 .htaccess rule to...

  • catch committed acf9193 on 8.3.x
    Issue #2508666 by alexpott, pwolanin, benjy: Drupal 8 .htaccess rule to...

  • catch committed acf9193 on 8.3.x
    Issue #2508666 by alexpott, pwolanin, benjy: Drupal 8 .htaccess rule to...

  • catch committed acf9193 on 8.4.x
    Issue #2508666 by alexpott, pwolanin, benjy: Drupal 8 .htaccess rule to...

  • catch committed acf9193 on 8.4.x
    Issue #2508666 by alexpott, pwolanin, benjy: Drupal 8 .htaccess rule to...
edward.radau’s picture

I believe this is the same issue I ran across recently. The issue is that the regex is not escaped properly. The rule needs to be written as follows:

RewriteRule "^(.+/.*|autoload)\.php($|//)" - [F]

The regex isn't escaped properly. If it isn't escaped the regex will be be far too broad and will match almost anything. It will NOT match "autoload" but many different strings. I haven't checked any of the patches yet in this thread but I am hoping it has already been addressed.

This issue still affects the latest version of D8

izmeez’s picture

Regarding backport to Drupal 7 I have looked over the patch that was committed to Drupal 8. The change to the FilesMatch line already exists in Drupal 7 and the file autoload.php is not in drupal 7. All that is left are the tests and I'm not sure it's worth the effort.

If I'm correct the backport is not needed or a new issue to specifically add tests for .htaccess in drupal 7 should be created.

poker10’s picture

Title: Drupal 7 .htaccess rule to prevent backup file access is broken and needs more tests » Drupal 8 .htaccess rule to prevent php file access can be easily bypassed
Version: 7.x-dev » 8.0.x-dev
Status: Patch (to be ported) » Fixed
Issue tags: -Needs backport to D7

Let's handle the backport in the child issue here: #2779833: Fix Drupal 7 .htaccess to protect .orig and .save files from view. It is not possible to completely backport this fix as D7 does not have PHP files execution protection (this is being discussed here: #1587270: Forbid execution of PHP files in subfolders by default (except those needed by core)), so only fix to .orig and .save extensions can be backported + test.

So I think it will be the best to close this as fixed for D8.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.