spectre.console: POSIX: Invalid handling of parameter values starting with double quotes (`"`), e.g. `--order-by='"quoted"` value'

Important: This is a follow-up issue (#891), since it was closed without possibility to give feedback, reopen or answering my follow-up questions. I really think this is clearly a bug of spectre and it should be further investigated, because input values are MODIFIED without any developer interaction… It may also be possible, that this leads to a SECURITY FLAW because parameters are mistreated, but I’m no expert.

This also may not be reproducible in any OS (e.g. on Windows) AND also not via Debugger or IDE, since command line parameter handling is different. To reproduce the issue, I recommend using Linux and compile the given test program in release mode.

The last answer I got stated this about argument parsing:

Unfortunately, this is out of our hands. We are just parsing the args that .NET gets from its command line handler. In fact, on Windows I get different results from you with different examples.

I think, this is wrong, because at least something in spectre seems to Trim whitespaces (Details: https://github.com/spectreconsole/spectre.console/issues/891#issuecomment-1179605527) beause the following example does not modify parameters, while spectre does.

// Program.cs, dotnet 6
    Console.WriteLine("== Environment.CommandLine ==");
    Console.WriteLine(Environment.CommandLine);
    Console.WriteLine("");

    Console.WriteLine("==  Environment.GetCommandLineArgs() ==");
    foreach(var a in Environment.GetCommandLineArgs()){
        Console.WriteLine(a);
    }
    Console.WriteLine("");

    Console.WriteLine("== args ==");
    foreach(var b in args){
        Console.WriteLine(b);
    }
    return 0;

Information

  • OS: Linux, Ubuntu 18.04 LTS
  • Version: 0.45
  • Terminal: xterm-256color, zsh

Describe the bug Parameter values cannot start with double quotes ("). They are either replaced or handled completely wrong. Additionally, parameter values are TRIMMED (remove leading spaces), so adding a space to workaround this is not possible.

The following example even cuts off the parameter value and treats everything after the space as EXTRA argument, in the following case value is handled as positional argument and not part of --order-by:

cli-tester test --order-by '"quoted" value' input.mp3

This may be a security flaw under specific circumstances.

More examples of invalid handling:

# `quoted` instead of `"quoted" value` and cut off handling as positional argument
cli-tester test --order-by '"quoted" value' input.mp3
cli-tester test --order-by='"quoted" value' input.mp3

# `quoted` instead of `"quoted"` 
cli-tester test --order-by '"quoted"' input.mp3

# `quoted` instead of ` "quoted"` 
cli-tester test --order-by ' "quoted"' input.mp3

The following examples work like expected, if not starting with double quotes or single quotes are used

# `value "quoted" like expected
cli-tester test --order-by 'value "quoted"' input.mp3

# `'quoted' value` like expected
cli-tester test --order-by "'quoted' value" input.mp3

# `\"quoted" value' value` like expected
cli-tester test --order-by='\"quoted" value' input.mp3

To Reproduce

Expected behavior I would expect double quotes (") and spaces ( ) CAN be a valid part of a parameter value and should not be replaced or parsed out in any way.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 29 (14 by maintainers)

Most upvoted comments

This is the combined PR @sandreas across the various command line parsing issues, in case you want to build and try it out: https://github.com/spectreconsole/spectre.console/pull/1048

I’ve just created this PR https://github.com/spectreconsole/spectre.console/pull/1036 @sandreas (and will fix any build errors that may appear…)

As mentioned in the PR, the following three PR’s when taken jointly represent a significant enhancement to existing command line parsing behaviour: https://github.com/spectreconsole/spectre.console/pull/1036, https://github.com/spectreconsole/spectre.console/pull/1029, https://github.com/spectreconsole/spectre.console/pull/732

If it would be useful, I could merge them all into a local ‘preview’ branch for you to build and test Tone against?

Oh, please don’t do that!

Ok fair enough. I’m just in the middle of mental gymnastics, trying to think of all the unit test cases to support. And what not to support.

My PR isn’t far away now so you will be able to try it out with your Tone soon enough…

Regarding handling of quotes, hyphens and spaces - do you see any cases I’ve missed?

Maybe. I don’t see the following (maybe this is intended):

  • "\"quoted\" string" (Quoted string that does not end with quotes)
  • " \"quoted\"" (space then quoted string)
  • "" (empty string)
  • " " (space only)
  • " " (multiple spaces - think I found a markdown bug in githubs parser - this also shows only one space 😉)
  • "-" (dash only)
  • "-size" (simple string with dash)
  • "\t" (tab)
  • "\r\n\t" (multiple whitespace that should never happen)
  • "👋🏻" (an emoji)
  • "🐎👋🏻🔥❤️" (multiple emojis)
  • "\"🐎👋🏻🔥❤️\" is an emoji sequence" (multiple emojis quoted with additional text)

I’m sorry ¯\_(ツ)_/¯… If you think one of these cases is not necessary, just leave it out 😉

You sir, deserve a cookie!

Maybe it would be a good idea to implement the args parser framework / library agnostic.

class SubCommand {
    public string Name;
}
class Flag {
    public string Name;
    public string[] Aliases; // e.g. for shortName
}

class Option {
    public string Name;
    public string[] Aliases; // e.g. for shortName
    public string? Value; // not a real type because every arg is at first a string - this can be done by the parser
}
class Argument {
    public int index;
    public string? Value; // not a real type because every arg is at first a string - this can be done by the parser
}
class SubCommand {
    public IEnumerable<SubCommand> SubCommands {get; set;}
    public IEnumerable<Flag> Flags {get; set;}
    public IEnumerable<Option> Options {get; set;}
    public IEnumerable<Argument> Arguments {get; set;}
}
var tokenizer = new Tokenizer("=");
var subCommands = new List<SubCommand>();
// configure subcommands with arguments and stuff
var parser = new ArgsParser(subCommands, tokenizer);
var result = parser.Parse(args);

Just a thought…

As part of fixing the ‘flags cannot be set’ issue, I introduced a HadSeparator flag on command tree token which allows the CommandTreeParser some knowledge of context, see here: https://github.com/spectreconsole/spectre.console/blob/dded816d97f5a6cbf9c1831aa00fe46684d1ac5e/src/Spectre.Console.Cli/Internal/Parsing/CommandTreeToken.cs

however, this is more of a tactical implementation when compared to your design suggestion above. I don’t know the codebase well enough to refactor extensively yet, however the command tree parser looks easily backed by an interface and dependency injected.

For now, I’ll see what I can do tactically to fix the unit tests, so it can be released sooner than later. Then look at perhaps a new implementation of the parser following a statemachine or similar. Which can be injected and validated with existing unit tests, instead of attempting open heart surgery on the currrent codebase.

Ok, I can reproduce what you have reported across several bug reports and the discussion thread (https://github.com/spectreconsole/spectre.console/discussions/1025) with the following unit test:

Awesome 😃

I don’t mind having a look into what may be required to fix these failing tests, which if successful, would be a massive step forward in addressing many edge case issues regarding command line argument handling.

Great, thank you very much. Maybe we should keep in mind that fixing these unit-tests are not gonna fix the design problem, that EVERY argument is going through the method CommandTreeTokenizer.Parse:

This design problem does not only result in unnecessary parsings (Performance, Security), but not regarding the previous type of Token for an argument leads to other bugs:

So maybe it’s worth also taking a look at:

I’m very excited that now it is clear, what I meant 😃 Thank you for your hard work.

This is a pretty extreme edge case - relying on shells to act consistently with quotes is simply a non-starter as the link in the previous issue talk about.

@phil-scott-78 While I think a problem in the Tokenizer / Parser logic is no extreme edge case any more and fixing this would take me deep knowledge about how spectre console parses, tokenizes and transfers args to Settings, am I really the right person to look at this?

I did not find any unit tests for the tokenizer… did I miss them? If not, how can I ensure I don’t break something when I try to fix this?

Sorry, I just want to prevent putting much work into something that is not useful to anyone.

I took a look some more in this. I still comes down to how each shell is gonna handle the quotes and each shell is wildly different. Cmd.exe, pwsh, and even running through the debugger all vary wildly in the scenarios you posted with no consistency. But, on your shell and only this shell it does include the quotes in the args and theoretically we could include them. Went ahead and created a unit test shows the failure.


    [Fact]
    public void Quote_Tests()
    {
        // Given
        var app = new CommandAppTester();
        app.Configure(config =>
        {
            config.PropagateExceptions();
            config.AddCommand<GenericCommand<FooCommandSettings>>("test");
        });

        // When
        var result = app.Run(new[]
        {
            "test", "\"Rufus\"",
        });

        // Then
        result.ExitCode.ShouldBe(0);
        result.Settings.ShouldBeOfType<FooCommandSettings>().And(foo => foo.Qux.ShouldBe("\"Rufus\""));
    }

This is a pretty extreme edge case - relying on shells to act consistently with quotes is simply a non-starter as the link in the previous issue talk about. If you or anyone else want to give fixing this bug a shot, consider it up for grabs though, and you are welcome to see if you can’t get the parser updated to take this scenario into account. But personally, this is not a behavior I’d ever rely on for end users and I would highly recommend something other than nested quotes in the arguments if you are going to have an app running in multiple shells…