Problem/Motivation

http://seclists.org/fulldisclosure/2015/Oct/42

While we block vendor with .htaccess, that's not foolproof

Proposed resolution

Delete vendor test code during composer install and update by implementing additional composer script hooks.

Code to be deleted is based on a list maintained in \Drupal\Core\Composer\Composer

Remaining tasks

final review & commit
re-install all dependencies with composer and commit with test code removed

User interface changes

n/a

API changes

n/a

CommentFileSizeAuthor
#76 2585165.74.patch6.28 MBalexpott
#73 2585165-3-72.patch6.27 MBalexpott
#61 increment-important-bits.txt580 bytespwolanin
#61 2585165-important-bits-61.patch5.96 KBpwolanin
#56 increment-important-bits.txt4 KBpwolanin
#56 2585165-important-bits-56.patch5.95 KBpwolanin
#46 increment-important-bits.txt1.97 KBpwolanin
#46 2585165-important-bits-46.patch6.36 KBpwolanin
#44 increment-important-bits.txt2.39 KBpwolanin
#44 2585165-important-bits-44.patch4.97 KBpwolanin
#35 2585165-35.patch6 MBgeertvd
#33 2585165-32.patch6 MBgeertvd
#33 interdiff-2585165-28-32.txt1009 bytesgeertvd
#28 don_t_include_vendor-2585165-28.patch6 MBtarekdj
#25 interdiff.txt740 bytestarekdj
#25 don_t_include_vendor-2585165-25.patch740 bytestarekdj
#22 2585165.22.patch6 MBalexpott
#18 2585165-18.patch6 MBalexpott
#16 2585165-16.the-important-bits.patch2.54 KBalexpott
#16 2585165-16.patch788.65 KBalexpott
#6 increment.txt314 bytespwolanin
#6 2585165-6.patch446.91 KBpwolanin
#4 2585165-3.patch446.92 KBpwolanin
#2 2585165-2.patch1.82 KBpwolanin
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

pwolanin created an issue. See original summary.

pwolanin’s picture

Status: Active » Needs review
FileSize
1.82 KB

Here's a start on a new composer script to make sure certain dirs are absent.

I think this is useful regardless of anything else as a backstop since users of Drupal may install the dev dependencies on a live site without realizing that's potentially a security problem.

dawehner’s picture

My personal recommendation would be:

* Drop /vendor entirely
* Add a hook_requirement when dev dependencies are installed which gives a warning
* Patch composer to be able to configure the default behaviour for `composer install` to be the --no-dev variant

pwolanin’s picture

Here's a patch that happens when I run composer install and locally commit the changes with patch #2 applied.

pwolanin’s picture

@dawhener I think you over-estimate the competence of most people to use composer at all - an easy solution here is to just put the no-dev version into git as alexpott suggested, but I think we should also rm known problem test dirs as per patch #2 so people using composer themselves get extra protection, including because they will need to run it for module installations

pwolanin’s picture

whitespace fix in core/lib/Drupal/Core/Composer/Composer.php

alexpott’s picture

When we original .htaccess fix we accepted that it was not perfect. For a start we should also generate a web.config file too for anyone using IIS. And we should have a note for users of other webservers and we should have note of how to move vendor out of webroot.

But given that these limitations were discussed when we did the original fix - is this really critical?

I think we should remove the require-dev dependencies from Composer - this represents about 5Mb of code uncompressed that just does not need to be there for 99.9% of sites. Yes it will mean that developers will have to get in the habit of installing composer and doing composer install --dev if they want to develop on Drupal but this is how other projects work. But even this is not really enough. Until vendor is outside of webroot we are putting our security in our dependencies hands.

greggles’s picture

For a start we should also generate a web.config file too for anyone using IIS. And we should have a note for users of other webservers and we should have note of how to move vendor out of webroot.

I think this is admirable and ideal, but not a good thing to intermix in this issue.

The web.config files that we ship get very little attention (e.g. #1543392: Add a web.config to the several directories similar to the .htaccess file is years old, currently 7 months stale and it is for defense-in-depth for another security issue).
The typical Nginx configuration is not vulnerable to this issue since executable files are usually listed explicitly and it's unlikely someone would explicitly list this file.

larowlan’s picture

My opinion, restructure so we have a web folder and keep vendor outside it. That's how every other composer based project gets around this. Fairly big at this stage, but everything else is brittle

alexpott’s picture

chx’s picture

What about scanning for test and tests case insensitive in vendor/ and nuke it?

pwolanin’s picture

@larowlan - we should make sure that's possible, but it would be very disruptive (and non-experts would not understand) if Drupal shipped with a docroot + vendor directory. It's too late for 8.x for that change, we debated it a few months ago when this first came up.

So, the patch here is an easy 1st pass fix. We can add more or follow up with chx's suggesiton.

pwolanin’s picture

@alexpott - Drupal has a *known* XSS vector in the vendor test code. I don't see the same in joomla

alexpott’s picture

Status: Needs review » Needs work

So let's go for the general approach but I think we need to fix the implementation so that it fails if it does not remove the directories. This is protect us from thinking we're secure if mink moves its tests.

We should use the postPackageInstall and postPackageUpdate events and if the package matches remove the specific folders and if we can't remove the folder it should throw and exception or something and fail hard.

alexpott’s picture

Also can we get a followup which is also an RC target to remove all tests using this script from the dependencies since this will greatly reduce the tarball size and the amount of space a D8 instance takes.

alexpott’s picture

Status: Needs work » Needs review
FileSize
788.65 KB
2.54 KB

Something like this.

alexpott’s picture

Relevant discussion on composer https://github.com/composer/composer/issues/4438

alexpott’s picture

Here's everything that should be removed... a 6mb patch! This is a significant saving for anyone still using ftp or the likes. It should also make the D8 package significantly smaller too.

Status: Needs review » Needs work

The last submitted patch, 18: 2585165-18.patch, failed testing.

pwolanin’s picture

#16 looks reasonable. Let's get some version of the that basic cleanup in asap.

Mixologic’s picture

This is blocked by #2573687: Setup GitHub OAuth for Composer on Testbots - the testbots will potentially run into githubs API limits, so that needs to be solved before we can ask the testbots to hit github.

alexpott’s picture

Status: Needs work » Needs review
FileSize
6 MB

The diff needs to be made with --binary.

@Mixologic thanks for this information. The current solution this issue is looking at is removing all the unnecessary test code from dependencies - that does not required drupalci to do anything funky with composer / github.

chx’s picture

Status: Needs review » Reviewed & tested by the community

My heart sings at this patch. So. much. removal!

The last submitted patch, 18: 2585165-18.patch, failed testing.

tarekdj’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
740 bytes
740 bytes

Error running composer update:
Fatal error: Call to undefined method Composer\DependencyResolver\Operation\UpdateOperation::getPackage()

Status: Needs review » Needs work

The last submitted patch, 25: don_t_include_vendor-2585165-25.patch, failed testing.

tarekdj’s picture

Oops! sent the wrong patch! The right one one the way

tarekdj’s picture

Interdiff-22-25:

--- a/core/lib/Drupal/Core/Composer/Composer.php
+++ b/core/lib/Drupal/Core/Composer/Composer.php
@@ -127,7 +127,7 @@ public static function ensureHtaccess(Event $event) {
    */
   public static function vendorTestCodeCleanup(PackageEvent $event) {
     $vendor_dir = $event->getComposer()->getConfig()->get('vendor-dir');
-    $package = $event->getOperation()->getPackage();
+    $package = $event->getOperation()->getTargetPackage();
     $package_name = $package->getName();
     if (isset(static::$packageToCleanup[$package_name])) {
       foreach (static::$packageToCleanup[$package_name] as $path) {
tarekdj’s picture

Status: Needs work » Needs review

The last submitted patch, 18: 2585165-18.patch, failed testing.

dawehner’s picture

It is a bit sad that /vendor/behat/mink/driver-testsuite is removed as it has quite some good examples, but for sure this is not at all a good reason to keep it.

The last submitted patch, 25: don_t_include_vendor-2585165-25.patch, failed testing.

geertvd’s picture

Now this is failing on composer install.
So apparently we need to use $package = $event->getOperation()->getPackage() for install and $package = $event->getOperation()->getTargetPackage() for update

Status: Needs review » Needs work

The last submitted patch, 33: 2585165-32.patch, failed testing.

geertvd’s picture

Status: Needs work » Needs review
FileSize
6 MB

right, --binary

pwolanin’s picture

Status: Needs review » Needs work

Actually - reconsidering one detail here.

We do NOT want to throw an exception since that means anyone tying to rebuild with --no-dev (including ourselves) will have that fail until additional core patches are applied.

alexpott’s picture

Status: Needs work » Needs review

@pwolanin did you actually test that? I think not. The advantage of the change I made in #16 is that this fires after each package is installed or updated. Not after everything. I've tested this and it works as expected.

The last submitted patch, 18: 2585165-18.patch, failed testing.

The last submitted patch, 25: don_t_include_vendor-2585165-25.patch, failed testing.

The last submitted patch, 33: 2585165-32.patch, failed testing.

tarekdj’s picture

@alexpott after running a composer update the following files are updated:

  • vendor/wikimedia/composer-merge-plugin/README.md
  • vendor/wikimedia/composer-merge-plugin/src/MergePlugin.php

Should we consider those changes in the patch ?

pwolanin’s picture

@tarkdj - I think you should be running composer install, not composer update?

@alexpott oh I see : static::$packageToCleanup[$package_name]

Clever, yes, that should be fine.

pwolanin’s picture

For patches here is would be helpful to just see the "important bits" as a patch. The committers can run composer install themselves and commit the test files removal without having a patch for it.

Also - I'm not sure about the logic of assuming something is a directory if it's not a file - we could have symlinks?

pwolanin’s picture

Here's a patch for the important bits. It handles symlinks and tracks success of each operation and throws an exception on failure.

To see it work locally I did:

rm -rf vendor
composer install

This works except we seem to have something writing out a new .gitignore and failing to fully remove vendor/mikey179/vfsStream/src/test

vendor/mikey179/vfsStream/src/test/resources/filesystemcopy/emptyFolder/
vendor/mikey179/vfsStream/src/test/resources/filesystemcopy/withSubfolders/subfolder2/
vendor/symfony-cmf/routing/.gitignore
vendor/symfony/psr-http-message-bridge/.gitignore
pwolanin’s picture

Status: Needs review » Needs work

The package name and the target dir may not be equal.

Fixing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
6.36 KB
1.97 KB

Weird - seems the package name is lower case, but the directory name is mixed case, and $package->getTargetDir() is empty. I'm confused about how this is working.

Amusingly, since OSX HFS is case-insensitive, the change to the Composer.php file patch works as desired on OSX HFS, but likely not on GNU/Linux ext3, xfs, etc.

Fixing also the package name to lower-case in our composer file might be the most consistent fix. Needs some testing on different machines, and this may end up appearing to be a move of files from mikey179/vfsStream to mikey179/vfsstream

dawehner’s picture

+++ b/core/composer.json
@@ -34,7 +34,7 @@
-    "mikey179/vfsStream": "~1.2",
+    "mikey179/vfsstream": "~1.2",

The package is uppercase, see https://packagist.org/packages/mikey179/vfsStream and https://github.com/mikey179/vfsStream/blob/master/composer.json

pwolanin’s picture

@dawehner - composer seems to disagree based on the output of $package_name = $package->getName();

pwolanin’s picture

It's looks like 1) case doesn't matter, but 2) the package name should be lower case 3) internally to composer it's converted to lower case.

re: #3 see: https://github.com/composer/composer/blob/master/src/Composer/Package/Ba...

$this->name = strtolower($name);

other info:

https://github.com/composer/packagist/issues/186
"Package names are now required to be lowercase."

And: https://getcomposer.org/doc/02-libraries.md
"While package names are case insensitive, the convention is all lowercase and dashes for word separation."

Note also that github seems to be case insensitive: https://github.com/mikey179/vfsstream/blob/master/composer.json works and git clone https://github.com/mikey179/vfsstream.git works.

dawehner’s picture

Well, beside that, isn't it still totally unimportant for this issue? Given that composer uses lowercase internally, everything should be alright. vfsstream has a gazillion of installations, I doubt anyone is not using some case sensitive filesystem. I'm sure that alexpott uses a proper filesystem for example.

alexpott’s picture

Yep I use a case sensitive file system.

dawehner’s picture

So let's please revert all those out of scope changes, please.

greggles’s picture

I would say that case insensitive file systems are pretty common. It's the default type for all Mac which is a common developer platform and I *believe* many (most?) Windows machines are also case insensitive.

I don't know if that's worth including code related to case sensitivity or not, just noting the discrepancy with the statements in #50.

pwolanin’s picture

@dawhenr - I don't understand your comment. We need to list the package name and maybe more important the git URL as lower case in our composer.json in order to install the package in the (lower case) location that the composer Package class says it will be.

pwolanin’s picture

Discussed with daweher in IRC - he didn't realize that in the current situation we are not finding the array element at all because the Package class lowercases the name, so I think he's ok with the composer.json change as an acceptable solution.

pwolanin’s picture

Here's an alternative to search over the array keys. Seems to work for me and hopefully dawehner will like it better.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thank you @pwolanin.

+++ b/core/lib/Drupal/Core/Composer/Composer.php
@@ -151,6 +151,35 @@ public static function vendorTestCodeCleanup(PackageEvent $event) {
+   * @param $package_name

nitpick, could be typehinted with string

The last submitted patch, 18: 2585165-18.patch, failed testing.

The last submitted patch, 25: don_t_include_vendor-2585165-25.patch, failed testing.

The last submitted patch, 33: 2585165-32.patch, failed testing.

pwolanin’s picture

catch’s picture

When we do #1475510: Remove external dependencies from the core repo and let Composer manage the dependencies instead these would no longer get packaged at all (assuming we keep require-dev out of tarballs which I think we should).

While this looks OK for a quick fix, what happens with these scripts if the library doesn't get installed at all? Also this looks like we'd need to manually maintain the list of libraries, for example if a library gets split into extra dependencies upstream (which happened recently with Zend Feed). Could some of the listing be parsed from composer itself?

pwolanin’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

@catch. The biggest problem would be solved if we skip the dev dependencies in git and the tarball.

See #37 we only check items that are installed or updated, so there is no problem if we don't have one of the packages in the list.

This patch is not a work of art but it would be good enough to get us to release if we do nothing else then commit this and then re-install the deps and commit them with tests removed. Yes, the list needs to be maintained, but it also gives us control to not remove some tests if e.g. we need to leverage them for core tests.

dawehner’s picture

@catch. The biggest problem would be solved if we skip the dev dependencies in git and the tarball.

Just skipping the dev dependencies in git is not a solution, people who want to run tests otherwise would need to composer install and then ignore some of the changes in /vendor

pwolanin’s picture

@dawehner - yes, you are right. It would still be likely that sites would pull in the dev dependencies and then deploy with the vulnerable code, which is why I like this patch as a safeguard.

Mile23’s picture

If you are managing the dependencies with composer, then you can say composer install --no-dev in your deployment process. Then you don't have mink at all.

If you're expecting to run the tests packaged with e.g. mink, then with the current patch you need to say composer install --prefer-source --no-scripts, which would leave the tests, but also fail to add all our htaccess and special-case autoload dumping. So then you need to do composer run-script pre-autoload-dump and composer run-script post-autoload-dump.

I vote for:

  1. Not adding the patch here.
  2. Doing composer install --no-dev on the packaging script for the tarball (with an unfortunate side effect of being unable to run PHPUnit).
  3. Finally removing the vendor/ directory from the repo. #1475510: Remove external dependencies from the core repo and let Composer manage the dependencies instead
  4. Adding a testbot step where we build with no-dev and run all the tests.
  5. Documenting best-practice install processes for different workflows.

I just did a composer install --no-dev and tried to run the phpunit_example tests in the simpletest UI. Result: WSOD. So some work is needed there before packaging the tarball without dev.

Mixologic’s picture

Adding a testbot step where we build with no-dev and run all the tests.

I think you mean build with --dev

Mile23’s picture

I think you mean build with --dev

Nope. :-) See point 2. We'd need coverage for --no-dev.

Mixologic’s picture

so adding a testbot step = run all the tests with both --dev and --no-dev? The testbots do not use packaged tarballs.

alexpott’s picture

@Mile23

If you're expecting to run the tests packaged with e.g. mink

Clone mink - don't clone Drupal and run any version of Drupal install.

alexpott’s picture

@Mixologic for obvious reasons testbot is only going to work with --dev

Mile23’s picture

Clone mink - don't clone Drupal and run any version of Drupal install.

Agreed.

@Mixologic for obvious reasons testbot is only going to work with --dev

Well, no. :-) We still have SimpleTest and it still works part-way for a user driving the UI. It will also work with run-tests.sh. Try it:

$ composer install --no-dev
$ php core/scripts/run-tests.sh --list

Simpletest should discover all the tests it can run without phpunit installed, which it currently doesn't do (it assumes phpunit is around).

Then the tarball can ship with --no-dev, so at least the security concern is gone there.

If someone wants to deploy with the repo, the onus is on them to do --no-dev for production, which they're used to doing anyway if they know composer at all.

But: This means a new behavior for the tarball which needs to be tested.

The only way to test that is with another testbot step that builds with --no-dev.

alexpott’s picture

@Mile23 the fact that Simpletest UI does not check whether of not PHPUnit exists is not relevant for this issue since this issue does not break it doing --no-dev breaks it.

Here's the full patch to commit.

Mile23’s picture

6 MB patch?

@Mile23 the fact that Simpletest UI does not check whether of not PHPUnit exists is not relevant for this issue since this issue does not break it doing --no-dev breaks it.

Right. It's relevant to my solution which is to build with --no-dev which exists to solve the problem of security holes in your test frameworks.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 73: 2585165-3-72.patch, failed testing.

alexpott’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
6.28 MB

@Mile23 building with --no-dev does not exist to fix security flaws in anything. Storing vendor outside of webroot should be recommended secure Drupal practice because even this fix, which deletes more test code than a --no-dev build, does nothing if there is security issue with direct access to a non testfile and the .htaccess file in vendor is not working.

The *real* issue is that composer should distinguish between distribution and source and distribution should only contain what is actually necessary for runtime and nothing else.

And yes this deletes 6mb of unnecessary test code. I just forgot the --binary switch again.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

  • catch committed 7afeeeb on 8.0.x
    Issue #2585165 by pwolanin, alexpott, tarekdj, geertvd, dawehner,...

Status: Fixed » Closed (fixed)

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

barryvdh’s picture

It is possible to distinguish source and distributed packages. This can be done with .gitattributes using the export-ignore feature. https://www.reddit.com/r/PHP/comments/2jzp6k/i_dont_need_your_tests_in_m...

This can be fixed upstream in Mink, for example see https://github.com/minkphp/Mink/pull/691

This is also what happened in one of the other packages; https://github.com/paragonie/random_compat/commit/e1ddb31788f16f4527074f...
That is a good thing, but broke your Composer plugin. It should not break when a folder is moved/renamed or exluded from dists. For example, when installing from source (--prefer-source) the tests folder will exist, but when using --prefer-dist, it's gone.
The packaged distribution should be dist, but when people are updating composer on their own (and hit API limits or something), you should take both cases into account.

bojanz’s picture