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

Fix command escaping #780

Merged
merged 8 commits into from
Jul 9, 2020
Merged

Fix command escaping #780

merged 8 commits into from
Jul 9, 2020

Conversation

1ed
Copy link
Contributor

@1ed 1ed commented Jun 27, 2020

Q A
Branch master
Bug fix? yes
New feature? no
BC breaks? yes
Deprecations? no
Documented? no
Fixed tickets #779, #733

The first commit what I need, the others are optional. I removed PhpCsFixerFormatter::formatSuggestion() which was public and not internal so I consider it as a BC break.

@1ed 1ed force-pushed the command-escaping branch from 65a293a to 7e16383 Compare June 27, 2020 15:07
Copy link
Contributor

@veewee veewee left a comment

Choose a reason for hiding this comment

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

Hello @1ed,

Thanks for the PR! This part of the tool is haunting us for some time, so I want to be carefull in 'how' we fix the issue.
That is why I added quite a lot of remarks and questions to the PR. Can you take a look at them?

@@ -60,6 +59,6 @@ private static function fixInternalComposerProcessVersion(
return $process;
}

return new Process(ProcessUtils::escapeArguments($commandlineArgs->getValues()));
return new Process($commandlineArgs->getValues());
Copy link
Contributor

Choose a reason for hiding this comment

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

Sadly, composer 1 and 2 comes shipped with a hardcoded symfony/process v2.8 dependency:
https://github.com/composer/composer/blob/master/composer.lock#L949-L950

The development integrator runs as a composer script in a php context which contains the composer dependencies rather than with the application's dependencies.
This means that this code won't work since you now pass an array instead of a string.

We could add a method ProcessArgumentsCollection::asCommandLineString() or something similar instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

@@ -28,7 +28,7 @@ public function getConfigTreeBuilder(): TreeBuilder
$gitHookVariables->addDefaultsIfNotSet();
$gitHookVariables->children()->scalarNode('VAGRANT_HOST_DIR')->defaultValue('.');
$gitHookVariables->children()->scalarNode('VAGRANT_PROJECT_DIR')->defaultValue('/var/www');
$gitHookVariables->children()->scalarNode('EXEC_GRUMPHP_COMMAND')->defaultValue('exec');
$gitHookVariables->children()->variableNode('EXEC_GRUMPHP_COMMAND')->defaultValue('exec');
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it is better to make it an arrayNode but to also accept string input. If it is a string input, the config is normalized into an array. (That should be possible with the symfony configuration component)
This change also needs to be added to the documentation.

@@ -74,7 +73,7 @@ function it_outputs_the_command_when_run_very_very_verbose(IOInterface $io)
$io->isVeryVerbose()->willReturn(true);

$command = '/usr/bin/grumphp';
$io->write([PHP_EOL . 'Command: ' . ProcessUtils::escapeArgument($command)], true)->shouldBeCalled();
$io->write([PHP_EOL . sprintf('Command: %1$s%2$s%1$s', '\\' === DIRECTORY_SEPARATOR ? '"' : "'", $command)], true)->shouldBeCalled();
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe instead of adding this directory separator logic : pass it through
Process::fromShellCommandline($command)->getCommandLine()
Instead?

Thay way it will always work the way the loaded symfony/process wants it to be formatted?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Process::fromShellCommandline not escapes the command. I added a check if the call to $io->write contains the command string, maybe that's enough

@@ -133,7 +133,8 @@ protected function parseHookBody(string $hook, string $templateFile): string
];

foreach ($this->hooksConfig->getVariables() as $key => $value) {
$replacements[sprintf('$(%s)', $key)] = ProcessUtils::escapeArgumentsFromString($value);
$process = is_array($value) ? new Process($value) : Process::fromShellCommandline($value);
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't it be better to add a configuration normalizer that converts a string into an array?
That way we can always assume we have an array, even if the grumphp.yaml contains a string.

I am not sure how this solves your issue though: it still escapes the configured CLI argument.
If you are on windows, it escapes with double quotes, so it might work.
However, if you are on linux: it escapes with single quotes, meaning it won't work.

Not sure if it's a good idea, but maybe we shouldn't escape the variables and leave that up to the one configurating grumphp? That way you can do something like:

grumphp:
    git_hook_variables:
        EXEC_GRUMPHP_COMMAND: ['docker-composer --rm --no-deps -u "$(id -u):$(id -g)"', "'php'" ]

Which will result in:

docker-composer --rm --no-deps -u "$(id -u):$(id -g)" 'php'

Meaning you can choose if you want to escape or not from the config file.
However: it means we won't escape by default anymore....

(Assuming it is a developer tool, this won't be a big security issue IMO.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In this way it's up to the developer if the command will be escaped or not. If someone uses a string it won't be escaped automatically, but if someone uses an array it will be escaped by the process component.
Won't doing automatic escaping would be the other option, but I think it's more convenient this way. It's up to the user to decide.

If it would be normalized to an array how could we decide if it needs escaping or not?

In my case I can use EXEC_GRUMPHP_COMMAND: 'docker-compose run --rm --no-deps -u $(id -u):$(id -g) php'

Copy link
Contributor

Choose a reason for hiding this comment

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

sounds good!

@@ -95,7 +95,7 @@ protected function initializeComposer(string $path): string
{
$process = new Process(
[
$this->executableFinder->find('composer'),
realpath($this->executableFinder->find('composer', 'composer')),
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you tell me what the issue is exactly?
Why are you defaulting to the string 'composer' if the file cannot be found?
I try to avoid realpath as much as possible in this package, since it already caused a lot of issues.

Copy link
Contributor Author

@1ed 1ed Jul 5, 2020

Choose a reason for hiding this comment

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

I have ...:vendor/bin:... in the $PATH so the executable finder finds that first and tries to run that with a relative path.

Copy link
Contributor

Choose a reason for hiding this comment

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

@janvernieuwe : Does this fix the issue you had while running paratest as well? It might be related

@@ -36,16 +35,6 @@ public function format(Process $process): string
return $this->formatJsonResponse($json);
}

public function formatSuggestion(Process $process): string
Copy link
Contributor

Choose a reason for hiding this comment

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

Agree we can get rid of this method. However, we should still suggest the fix command. (See further in CR)

],
],
]);

$this->installComposer($this->rootDir);

$hookPattern = '{[\'"]?'.preg_quote($php, '{').'[\'"]? [\'"]?-d[\'"]? [\'"]?date\.timezone=Europe/Brussels[\'"]?}';
$hookPattern = sprintf('{[\'"]%s[\'"] [\'"]-d date\.timezone=Europe/Brussels[\'"]}', $php);
Copy link
Contributor

Choose a reason for hiding this comment

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

better to keep $php preg_quote-ed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added back

@@ -102,7 +102,7 @@ public function run(ContextInterface $context): TaskResultInterface

if (!$process->isSuccessful()) {
$messages = [$this->formatter->format($process)];
$fixerCommand = $this->formatter->formatSuggestion($process);
$fixerCommand = $process->getCommandLine();
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of creating the suggestion in the formatter, we can add arguments to the $arguments variable as we do in the eslint task:
https://github.com/phpro/grumphp/blob/master/src/Task/ESLint.php#L92-L105

This way, we are still suggesting the fix command to the end user in case of a detected fixable item.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

@1ed 1ed force-pushed the command-escaping branch from 7e16383 to 2ca14d4 Compare July 5, 2020 01:08
Copy link
Contributor

@veewee veewee left a comment

Choose a reason for hiding this comment

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

The code looks good and I like the way we can now configure the command! Thanks!

@@ -133,7 +133,8 @@ protected function parseHookBody(string $hook, string $templateFile): string
];

foreach ($this->hooksConfig->getVariables() as $key => $value) {
$replacements[sprintf('$(%s)', $key)] = ProcessUtils::escapeArgumentsFromString($value);
$process = is_array($value) ? new Process($value) : Process::fromShellCommandline($value);
Copy link
Contributor

Choose a reason for hiding this comment

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

sounds good!

@veewee veewee added this to the 0.19.2 milestone Jul 9, 2020
@veewee veewee merged commit 8457d88 into phpro:master Jul 9, 2020
@1ed 1ed deleted the command-escaping branch February 1, 2021 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants