-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Golang : Add query to detect JWT signing vulnerabilities #9378
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
Conversation
randint.getTarget().hasQualifiedName("crypto/rand", "Int") and | ||
bigint.getTarget().hasQualifiedName("math/big", "Int", "Int64") and | ||
bigint.getReceiver() = randint.getResult(0).getASuccessor*() and | ||
r.reads(this, bigint.getAResult().getASuccessor*()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is similar enough to the previous case, you could replace getASuccessor*
with an augmented get-a-successor that includes the taint step across big.Int.Int64
(or always include that as a taint step, and use localTaint like the case below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I follow. Do you mean to say I should create a new taint tracking config and add this step there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I mean instead of using getASuccessor* aka localFlow to track flow from rand.Int() etc to Int.Int64() and then using it again to track from Int64() to an array-read, you could extend AdditionalTaintStep
or FunctionModel
to make Int64()
taint-conserving and then use a single localTaint call to track flow from rand.Int()
through to an array-read that you want to sanitize. Similarly you could make %
modulus operations taint-conserving so they wouldn't have to be sanitized.
One other related question: it looks like in your sanitizers you sanitize the input to constant[random]
, whereas logically you should probably sanitize the result instead.
The ideal version would look something like
using source = post-update-node of rand.Read(_) or result of rand.Int() etc, and additional taint steps = big.Int functions and modulus operations, sanitize track taint forwards to any index operand and sanitize that array-read's result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smowton I think I partially understand what you wish to say. I have made some changes did you mean something on these lines?
@smowton Changes done! PTAL. |
( | ||
this.(DataFlow::ElementReadNode).reads(_, randomValue) or | ||
any(DataFlow::ElementReadNode r).reads(this, index) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
( | |
this.(DataFlow::ElementReadNode).reads(_, randomValue) or | |
any(DataFlow::ElementReadNode r).reads(this, index) | |
) | |
this.(DataFlow::ElementReadNode).reads(_, index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smowton That would make the tests fail. Please see the test file for the cases which fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not; I've re-verified that this works and will push it myself shortly along with some other last cleanups.
It's possible you're thinking of this.(DataFlow::ElementReadNode).reads(_, randomValue)
which indeed does not work because we didn't follow the taint trail to index
.
private class CompareExprSanitizer extends Sanitizer { | ||
CompareExprSanitizer() { | ||
exists(BinaryExpr c | | ||
c.getAnOperand().getGlobalValueNumber() = this.asExpr().getGlobalValueNumber() | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried removing this sanitizer, and it doesn't seem to be useful:
- It handles a few cases where an empty string is returned alongside an error value, but this would be better handled by looking for the pattern of an empty string
""
guarded by a testif x != nil
, wherex
is of error type. - The other two cases were truly unreachable code:
x := "hello"; if x != "hello" { ... }
, which doesn't seem like a useful test (but perhaps you could change the test to better represent a real-world situation?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It handles a few cases where an empty string is returned alongside an error value, but this would be better handled by looking for the pattern of an empty string "" guarded by a test if x != nil, where x is of error type.
This may not be ideal. There could be multiple instances where a error code may be returned without a test to nil, say for ex:
func t()(string, error){
if config.hasKey(){
key:= config.getKey()
return key,nil
} else {
return getDefaultKey(), errors.New("no key")
}
To avoid this, I just check for cases where there the taint is compared with anything and mark that as sanitized. This handles the if x!=nil
and if y !=""
cases very well.
The other two cases were truly unreachable code: x := "hello"; if x != "hello" { ... }, which doesn't seem like a useful test (but perhaps you could change the test to better represent a real-world situation?)
I have added a better test case now. The test case for this sanitizer was earlier in main.go
. I have moved that to sanitizer.go
. Also, I have removed the test-case you mention.
/** Mark an empty string returned with an error as a sanitizer */ | ||
private class EmptyErrorSanitizer extends Sanitizer { | ||
EmptyErrorSanitizer() { | ||
exists(ReturnStmt r, DataFlow::CallNode c | | ||
c.getTarget().hasQualifiedName("errors", "New") and | ||
r.getNumChild() > 1 and | ||
r.getAChild() = c.getAResult().getASuccessor*().asExpr() and | ||
r.getAChild() = this.asExpr() | ||
) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removing this has no effect on the test results, so it either doesn't work or isn't tested
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added a better test-case now.
- Document - Only actually consider comparisons - Don't sanitize literals
…nedAlongsideErrorSanitizerGuard
Just pushed the remaining changes I'd want to see in the interests of getting this merged before I go on holiday. Please take a look over them; will merge this to experimental after CI completes. |
@smowton Looks good to me. Please don't forget to move the bounty application to the next stage after the merge. Happy Holidays! |
Thanks for the quick TAT @smowton |
Supersedes github/codeql-go#705