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

SONARKT-569 Add support for WebViews to S4830 #572

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

antonioaversa
Copy link
Contributor

@antonioaversa antonioaversa commented Feb 20, 2025

@antonioaversa antonioaversa force-pushed the antonio/SONARKT-569-S4830 branch 2 times, most recently from 97c6a03 to 5a73038 Compare February 20, 2025 16:44
@antonioaversa antonioaversa force-pushed the antonio/SONARKT-569-S4830 branch from 5a73038 to 42d89ca Compare February 20, 2025 16:51
@antonioaversa antonioaversa marked this pull request as ready for review February 20, 2025 16:56
}

override fun visitExpression(expression: KtExpression) {
potentialBranching = potentialBranching ||

Choose a reason for hiding this comment

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

Educational question: does this cover all branching?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, good question - the answer is: all the ones I could think of :-)
I find unlikely that somebody will find a way to have branching, have proceed and not have any cancel, in other ways... but I would not underestimate people creativity :-)
Anyway, unless I am missing some common scenarios, the risk for FP seems close to zero to me.
If we are concerned about this heuristic we can always remove it go for the Option 3 we discussed (only simple proceed in body), but that seems too much on the side of safety, at the expense of reach.
WDYT?

Copy link
Contributor

@leveretka leveretka left a comment

Choose a reason for hiding this comment

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

Nice work! I see we can have a few improvements here.

Comment on lines 129 to 137
private val androidSslErrorHandlerProceedFunMatcher = FunMatcher {
definingSupertype = "android.webkit.SslErrorHandler"
withNames("proceed")
}

private val androidSslErrorHandlerCancelFunMatcher = FunMatcher {
definingSupertype = "android.webkit.SslErrorHandler"
withNames("cancel")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's move matchers to the file level. Otherwise, we create them every time AndroidWebViewCandidateVisitor is instantiated

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to keep them local to the context where they are relevant. I should have probably created a private companion object, but maybe it's too much here. So I have moved them to the top of the file, as suggested.

Comment on lines 144 to 147
val isProceedCall = androidSslErrorHandlerProceedFunMatcher.matches(expression)
val isCancelCall = androidSslErrorHandlerCancelFunMatcher.matches(expression)
foundProceedCall = foundProceedCall || isProceedCall
foundCancelCall = foundCancelCall || isCancelCall
Copy link
Contributor

Choose a reason for hiding this comment

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

here we always try to match, both proceed and cancel and always override the foundProceedCall and foundCancelCall.

It seems to me we can inline

Suggested change
val isProceedCall = androidSslErrorHandlerProceedFunMatcher.matches(expression)
val isCancelCall = androidSslErrorHandlerCancelFunMatcher.matches(expression)
foundProceedCall = foundProceedCall || isProceedCall
foundCancelCall = foundCancelCall || isCancelCall
foundProceedCall = foundProceedCall || androidSslErrorHandlerProceedFunMatcher.matches(expression)
foundCancelCall = foundCancelCall || androidSslErrorHandlerCancelFunMatcher.matches(expression)

Copy link
Contributor

Choose a reason for hiding this comment

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

Also since these calls can't be matched together, we can refactor to only match one. I'll let you decide how is better to refactor it.

@@ -104,6 +125,43 @@ class ServerCertificateCheck : AbstractCheck() {
}
}

private class AndroidWebViewCandidateVisitor : KtVisitorVoid() {
Copy link
Contributor

Choose a reason for hiding this comment

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

That's FYI, Looks like for this rule we implemented recursive visit function by ourselves, however, there's a KtTreeVisitorVoid, which is recursive. No need to refactor this check, just FYI for the future rules.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have changed the code to useKtTreeVisitorVoid everywhere, so that I could remove the logic to traverse descendants. Except for the call to super, it was a drop-in replacement.

Comment on lines 63 to 68
val javaCryptographyExtensionCandidate = javaCryptographyExtensionFunMatchers.any { it.matches(function) }
&& !function.callsCheckTrusted()
&& !function.throwsCertificateExceptionWithoutCatching()
) {
val androidWebViewCandidate = androidWebViewFunMatchers.any { it.matches(function) }
&& function.callsProceedUnconditionally()
if (javaCryptographyExtensionCandidate || androidWebViewCandidate) {
Copy link
Contributor

@leveretka leveretka Feb 21, 2025

Choose a reason for hiding this comment

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

Here we match function to both FunMatchers regardless of the result. I'd suggest extracting refactoring the code so we don't match function a second time if we know it was already matched to something else. As an idea it can look like this:

when {
            javaCryptographyExtensionFunMatchers.any { it.matches(function) } ->
                if (!function.callsCheckTrusted() && !function.throwsCertificateExceptionWithoutCatching())
                    kotlinFileContext.reportIssue(function.nameIdentifier ?: function,
                        "Enable server certificate validation on this SSL/TLS connection.")
            androidWebViewFunMatchers.any { it.matches(function) } ->
                if (function.callsProceedUnconditionally()) {
                    kotlinFileContext.reportIssue(function.nameIdentifier ?: function,
                        "Enable server certificate validation on this SSL/TLS connection.")
                }
        }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, that's unnecessary computation.
I came up with something slightly different to avoid repeating reporting logic twice:

    override fun visitNamedFunction(function: KtNamedFunction, kotlinFileContext: KotlinFileContext) {
        val match = when {
            javaCryptographyExtensionFunMatchers.any { it.matches(function) } ->
                !function.callsCheckTrusted() && !function.throwsCertificateExceptionWithoutCatching()
            androidWebViewFunMatchers.any { it.matches(function) } ->
                function.callsProceedUnconditionally()
            else -> false
        }
        if (match) {
            kotlinFileContext.reportIssue(function.nameIdentifier ?: function,
                "Enable server certificate validation on this SSL/TLS connection.")
        }
    }

private fun KtNamedFunction.callsProceedUnconditionally(): Boolean {
val visitor = AndroidWebViewCandidateVisitor()
this.acceptRecursively(visitor)
return visitor.foundProceedCall && !visitor.foundCancelCall && !visitor.potentialBranching
Copy link
Contributor

Choose a reason for hiding this comment

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

That's more a matter of taste, what do you think about this way:

with(visitor) { foundProceedCall && !foundCancelCall && !potentialBranching }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, nice! I used it on the other visitor as well, for consistency.

Comment on lines 75 to 77
/*
* Returns true only when the function throws a CertificateException without a catch against it.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's maybe keep this Kdoc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I removed it because it seemed redundant - it basically says the same thing as the name of the function, without any further detail.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants