-1

I have this function to validate command in php?

public function validate_command($command) {
    if (isset($this->config->settings->guest_commands)) {
        $commands = $this->config->settings->guest_commands;
    } else {
        $commands = array('echo', 'cat', 'ls', 'find', 'cd', 'grep', 'test', 'xargs');
    }
    $cmd_re = "(" . implode("|", array_diff($commands, array("xargs"))) . ")";
    if (in_array("xargs", $commands)) {
        $re = "/^\s*($cmd_re|xargs\s*$cmd_re)/";
    } else {
        $re = "/^\s*$cmd_re/";
    }
    $separators = "/(&&|\|\||\||;)/";
    $parts = preg_split($separators, $command, null, PREG_SPLIT_DELIM_CAPTURE);
    $result = array();
    foreach ($parts as $part) {
        if (!preg_match($re, trim($part)) && !preg_match($separators, $part)) {
            $last = array_pop($commands);
            $message = "guest user can only execute: " .
                     implode(", ", $commands) . " and " . $last;
            throw new Exception($message);
        } else if (preg_match('/(>|`|\$\()/', $part)) {
            throw new Exception("guest user can't use redirect to write to files" .
                                " or execute subshell");
        } else {
            $result[] = $part;
        }
    }
    return implode($result);
}

it splits command using:

    $separators = "/(&&|\|\||\||;)/";
    $parts = preg_split($separators, $command, null, PREG_SPLIT_DELIM_CAPTURE);

How can I make it work for commands like echo "©"? it should return the same command but it throw exception.

Can I use single regex with look behind? How it should look like? It should work with cases like this:

echo "©\"©" && echo "©"

$parts should have array('echo "©\"©"', '&&', 'echo "©"') (the spaces around && or commands can be included)

I've try $separators = "/(?<![\"'](?:[^\"']|\\[\"'])*)(&&|\|\||\||;)/"; but got exception:

preg_split(): Compilation failed: lookbehind assertion is not fixed length at offset 24

Is iterating over the string the only option?

revo
  • 47,783
  • 14
  • 74
  • 117
jcubic
  • 61,973
  • 54
  • 229
  • 402
  • That preg_split() error is because of this `[\"'])*` part in lookbehind. Lookbehind are [zero length assertions](http://www.regular-expressions.info/lookaround.html). You cannot add quantifiers to them. – Rahul Apr 28 '17 at 07:39
  • You want to treat `©` as `&`? Replace it then. Or add `©©` to the regex pattern. – Wiktor Stribiżew Apr 28 '17 at 07:47
  • @WiktorStribiżew want to ignore split regex if it's in single or double quotes. – jcubic Apr 28 '17 at 07:55
  • So, match them and skip - prepend your regex pattern with [`(?:"[^"]*"|'[^']*')(*SKIP)(*F)|`](https://ideone.com/i9YAsE) – Wiktor Stribiżew Apr 28 '17 at 08:04
  • @WiktorStribiżew it works, yu can add an answer but I've used `"/1(?:\"(?:[^\"]|\\\")*\"|'(?:[^']|\\')*')(*SKIP)(*F)|(&&|\|\||\||;)/"` – jcubic Apr 28 '17 at 08:17
  • See revo's answer, I think it will work the same (but it is more efficient than your pattern). I simplified the regex in the comment to just showcase the approach. `\K` will not omit the match, it will return an empty match, but `PREG_SPLIT_NO_EMPTY` will remove these empty matches. – Wiktor Stribiżew Apr 28 '17 at 08:18

1 Answers1

3

You should match everything between quotes then exclude them from result set:

$re = <<< 'RE'
~(?:"[^"\\]*(\\.[^"\\]*)*"|'[^'\\]*(\\.[^'\\]*)*')\K|(&&|\|\||\||;)~
RE;

$str = <<< 'STR'
echo '"&co\"py;"'
STR;

var_dump(preg_split($re, $str, -1, PREG_SPLIT_NO_EMPTY));

Output:

array(1) {
  [0]=>
  string(17) "echo '"&co\"py;"'"
}
revo
  • 47,783
  • 14
  • 74
  • 117
  • what is `\K` in regex? – jcubic Apr 28 '17 at 08:20
  • It forces engine to reset parts of string which are matched so far from output. See [`\K` match resetter](http://stackoverflow.com/documentation/regex/1338/match-reset-k#t=201704280823386969309) – revo Apr 28 '17 at 08:24
  • I should mention `(&&|\|\||\||;)` can be reduced to `(&&|\|+|;)` if you know three consecutive pipes or more wouldn't be possible to come in commands. – revo Apr 28 '17 at 08:29
  • I don't know how it should work with `|||`. I think it would be better to just use `\|{1,2}` – jcubic Apr 28 '17 at 08:40
  • it return wrong results for sting `echo "©\"&;" "&& rm" && echo ls` – jcubic Apr 28 '17 at 08:51
  • What should be the desired output? – revo Apr 28 '17 at 08:52
  • it should return 3 elements: `['echo "©\"&;" "&& rm"', '&&', 'echo ls']`, same as Wiktor solution. – jcubic Apr 28 '17 at 08:55
  • So you need to exclude consecutive quoted arguments as well: `~(?:\s*(?:"[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(\\.[^'\\]*)*'))+\K|\s+(&&|\|{1,2}|;)\s+~` – revo Apr 28 '17 at 09:01
  • I've decide to use @WiktorStribiżew solution it's much shorter, it fit into one line in my code. – jcubic Apr 29 '17 at 11:08
  • what edge cases, can you give example that will fail with Wiktor regex and will work with yours? Maybe I should use yours instead. – jcubic Apr 30 '17 at 16:06
  • Like it won't match escaped quotes inside quotes of the same type. – revo Apr 30 '17 at 18:31
  • What about my modified regex: `"/(?:\"(?:[^\"]|\\\")*\"|'(?:[^']|\\')*')(*SKIP)(*F)|(&&|\|‌​\||\||;)/"` it have `"(?:[^"]|\\")*"` – jcubic Apr 30 '17 at 19:03
  • Considering regex of matching double quoted strings, [it doesn't match properly](https://regex101.com/r/dnwKDg/1). – revo Apr 30 '17 at 19:40
  • Yea but it matches `"©\\\\"&;"` too while it shouldn't. – revo Apr 30 '17 at 20:08
  • your last regex return single element array for string echo "asd \"asd" && rm it should return ['echo "asd \"asd"', " && ", 'rm'] – jcubic Apr 30 '17 at 22:45
  • That's not about regex but how PHP `preg_split` works. [**You should add a flag**](https://3v4l.org/HOu27). I'm not sure about your PHP level, but accepting and unaccepting an answer for several times is not a good idea and violates professional ethics. *Ask before judging if you don't know how things work.* – revo May 01 '17 at 15:22