Repository: trifectatechfoundation/sudo-rs (Rust)

Issue:
Order-dependency/inconsistency in sudoers parameter=value parsing wrt. quotes
**Describe the bug**
I'm testing sudo-rs, and came across a bit of weirdness in `sudoers` parsing, related to quotes and parameter order.

**To Reproduce**
1. `$ touch test` 
2. edit `/etc/sudoers.d/90-ssh-auth-sock` to look like this: 
    `Defaults!/bin/chown timestamp_timeout=1,env_keep+=SSH_AUTH_SOCK`

3. `$ sudo-rs chown root:root test # this works as expected`
4. edit `/etc/sudoers.d/90-ssh-auth-sock` to reorder the parameter=value pairs like this: 
    `Defaults!/bin/chown env_keep+=SSH_AUTH_SOCK,timestamp_timeout=1`

5. `$ sudo-rs chown jani:jani test # this fails:`
    `/etc/sudoers.d/90-ssh-auth-sock:1:63: double quotes are required for VAR=value pairs`
    `Defaults!/bin/chown env_keep+=SSH_AUTH_SOCK,timestamp_timeout=1`
    `                                                              ^`

**Expected behavior**

For sudo-rs to perform the command in 5. without error, as it did in point 3.

**Environment (please complete the following information):**
 - Ubuntu 24.04
 - `sudo-rs` commit hash: b434d4d (precompiled version 0.2.8 binary from the Github release page)

**Additional context**
For background, I'm using [pam_ssh_agent_auth](https://linux.die.net/man/8/pam_ssh_agent_auth) to authorize my user with SSH keys to run some commands, which requires `env_keep+=SSH_AUTH_SOCK`.

I also like have it time out immediately, so I additionally set `timestamp_timeout=0`. I initially thought the issue was caused by the zero, but testing with `timestamp_timeout=1` resulted in the same errors, so that's what I'm using here, for unambiguity.

The caret in the error message points to `timestamp_timeout`'s value, so I'd assume the logical solution is to quote that value, like this:

    Defaults!/bin/chown env_keep+=SSH_AUTH_SOCK,timestamp_timeout="1"

But this doesn't help:

    $ sudo-rs chown jani:jani test
    /etc/sudoers.d/90-ssh-auth-sock:1:63: double quotes are required for VAR=value pairs
    Defaults!/bin/chown env_keep+=SSH_AUTH_SOCK,timestamp_timeout="1"
                                                                  ^
So my next thought is to quote both values:

    Defaults!/bin/chown env_keep+="SSH_AUTH_SOCK",timestamp_timeout="1"

This causes a different error:

    $ sudo-rs chown jani:jani test
    /etc/sudoers.d/90-ssh-auth-sock:1:65: expected nonnegative number
    Defaults!/bin/chown env_keep+="SSH_AUTH_SOCK",timestamp_timeout="1"
                                                                    ^

The only remaining option is to quote only the first parameter value. Surprisingly, this works:

    Defaults!/bin/chown env_keep+="SSH_AUTH_SOCK",timestamp_timeout=1

    $ sudo-rs chown jani:jani test
    $

With OG sudo, any order or combination of these parameters, quoted or unquoted, works as expected.


Discussion:
Thanks for the extensive report!

I'm assuming that if you split this into multiple lines:
```
Defaults!/bin/chown env_keep+=....
Defaults!/bin/chown timestamp_timeout=...
```
it also works, in any order?

This looks like the tokenizer for values in `Defaults` isn't robust when stuffing multiple settings on the same `Defaults` line (my guess is we probably accept the `,` unquoted, but we should like into the code).
Yes, it seems to work as expected (i.e. identically with both OG and sudo-rs) when the are lines split, in either order, as long as  `timestamp_timeout` value is unquoted. Double-quoting it again causes the "expected nonnegative number" error (regardless of whether that line is first or second).

    $ sudo-rs chown jani:jani test
    /etc/sudoers.d/90-ssh-auth-sock:2:39: expected nonnegative number
    Defaults!/bin/chown timestamp_timeout="0"
                                          ^
    $ sudo cat /etc/sudoers.d/90-ssh-auth-sock 
    [sudo] password for jani:
    Defaults!/bin/chown env_keep+="SSH_AUTH_SOCK"
    Defaults!/bin/chown timestamp_timeout="0"
Interesting that ogsudo allows quoting the integer value. I should dive into the docs for ogsudo to see if that's someting to add to our 'undocumneted sudo behaviour' list. There's no reason to put the quotes there, as well.

But putting multiple defaults on one line is something that should work!
So it turns out that the tokenizer for environment variables is very liberal (since it's not well-defined what characters are allowed in an environment variable in Linux). I.e. in the above case the parser would think that the `env_keep` list is extended with the environment variable `SSH_AUTH_SOCK,timestamp_timeout` as long as its value is set to `1`. But by the rules for the /etc/sudoers syntax such a thing always needs to be surrounded by quotes (hence the error message).

If there was a space before the comma, the config would have worked; if there had been a space after the comma, the parser would have gotten completely confused and given up.

The fix was easy: don't allow unescaped "," as part of env var names.

Note that while writing the parser, there have been essentially two approaches that we used for various cases:
1. Be very strict, see if that works for users and then loosen the restraints (we have done that for usernames and hostnames for instance)
2. Be very liberal, and tighten if users report errors

For both cases, bug reports such as these are incredibly useful, so again thanks.



Patch:
diff --git a/src/sudoers/tokens.rs b/src/sudoers/tokens.rs
index c7b83d364..070912eb3 100644
--- a/src/sudoers/tokens.rs
+++ b/src/sudoers/tokens.rs
@@ -296,7 +296,7 @@ impl Token for EnvVar {
 
     const ALLOW_ESCAPE: bool = true;
     fn escaped(c: char) -> bool {
-        matches!(c, '\\' | '=' | '#' | '"')
+        matches!(c, '\\' | '=' | '#' | '"' | ',')
     }
 }
 


Test patch:
diff --git a/src/sudoers/test/mod.rs b/src/sudoers/test/mod.rs
index d35e1a3e2..688d94a3e 100644
--- a/src/sudoers/test/mod.rs
+++ b/src/sudoers/test/mod.rs
@@ -338,7 +338,7 @@ fn default_multi_test() {
     let (mut sudoers, _) = analyze(
         Path::new("/etc/fakesudoers"),
         sudoer![
-        "Defaults !env_editor, use_pty, secure_path=/etc, env_keep = \"FOO BAR\", env_keep -= BAR"
+        "Defaults !env_editor, use_pty, env_keep = \"FOO BAR\", env_keep -= BAR, secure_path=/etc"
     ],
     );
     sudoers.specify_host_user_runas(