The "previous" exception object can not be escaped
gmazzap opened this issue · 2 comments
Bug Description
We can only escape strings. Exception constructors accept a third argument "previous" which is an exception object and can not be escaped.
I would expect EscapeOutputSniff
to only look at the first argument of exception constructors.
Minimal Code Snippet
throw new \Exception(esc_html("Something when wrong"), 0, $previous);
throw new \Exception(esc_html("Something when wrong"), previous: $previous);
throw new \Exception(message: esc_html("Something when wrong"), previous: $previous);
throw new \Exception(previous: $previous);
Error Code
All the throw
statements above emit: WordPress.Security.EscapeOutput.ExceptionNotEscaped
Custom Ruleset
N/A
Environment
Question | Answer |
---|---|
PHP version | all |
PHP_CodeSniffer version | ^3.9.2 |
WordPressCS version | ^3.1.0 |
PHPCSUtils version | ^1.0.11 |
PHPCSExtra version | ^1.2.1 |
WordPressCS install type | Composer global, Composer project local |
IDE (if relevant) | -- |
Additional Context (optional)
I saw that the EscapeOutputSniff
class finds the parameter used to construct the exception, then it loops all of them to search for escaping tokens.
I think this issue could be solved only looking at the 1st positional parameter or message
, if named. And so replace usage of:
$params = PassedParameters::getParameters( $this->phpcsFile, $call_token );
with:
$messageParam = PassedParameters::getParameter( $this->phpcsFile, $call_token, 1, 'message' );
And then check the parameter, if found.
I'm glad to provide a PR that does this (and test), if wanted.
Tested Against develop
Branch?
- I have verified the issue still exists in the
develop
branch of WordPressCS.
@gmazzap Thanks for reporting this.
I'm a bit in two minds about this (which is, I suspect, also why I implemented it like it currently is).
The thing is, constructors for Exceptions can have any arguments. Exception constructors don't have to follow the same argument pattern as commonly used by the PHP native exceptions.
Additionally, it is also a common pattern to use throw MyException::create($arg1, $arg2, $arg3)
, where the parameters are all used in a sprintf()
to create the exception message.
So, blindly only checking the first parameter is not the right solution as it presumes use of the PHP native exceptions and/or of exceptions which have the same arguments as the PHP native exceptions for their constructor....
Here's the commit in which the escaping check for exceptions being thrown was added: 90f291c
Thanks @jrfnl
Additionally, it is also a common pattern to use throw MyException::create($arg1, $arg2, $arg3), where the parameters are all used in a sprintf() to create the exception message.
I would need to look at data to agree with that, because if I stick to my personal experience, static constructors very rarely use all string parameters. When parameters are supposed to be used with sprintf
(which is indeed common in my experience as well) they are often numbers (that don't need escaping) or objects (whose methods call results are used in sprintf
, but you can't escape when they are passed).
Not to mention that, often, named constructors also support a $previous
exception they then pass internally to the default constructor, even because otherwise it is not possible to set a $previous
exception at all after the exception is instantiated.
That means:
- For normal constructor having more than one argument the chance of a false positive is 100%
- For static constructor is very high
Summing up the two, the total chance of this rule creating false positives with exceptions using more than one arg is extremely high.
That, added to the fact that the use case for this rule is pretty narrow to begin with (basically it helps mitigate a server misconfiguration) makes me think that always escaping all arguments is definitively overkill.
I want to ask: do you foresee the possibility of introducing configurations for:
- only looking at the first argument/
message
named argument for default constructors - ignore anything that does not follow
throw new
(so only targeting default constructors).
If you agree to have these two configurations, I volunteer to write the code.