-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Python: CWE-117 Log injection #6182
Conversation
Hi @haby0, thanks for this contribution 👍 We'll take a look at this as soon, but we do have a few other things we're also working on at the moment 😊 |
Ok, thanks for your reply. |
@yoff The conflict has been resolved, please review, thank you. |
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.
You are not handling HTML logging and encoding, did you plan to? It might require a second configuration, seeing as the sinks dictate which sanitizers are required...
* See https://docs.python.org/3/library/logging.html#logger-objects | ||
*/ | ||
private class LogOutputMethods extends string { | ||
LogOutputMethods() { this in ["info", "error", "warn", "warning", "debug", "critical"] } |
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.
We seem to be missing "debug", "log" , and "exception" from this list.
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.
Pr updated.
this = API::moduleImport("logging").getMember(any(LogOutputMethods m)).getACall() | ||
} | ||
|
||
override DataFlow::Node getAnInput() { result = this.getArg(_) } |
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.
Note that getArg(_)
, perhaps confusingly, will not return all arguments. In particular, starred arguments are filtered out, as are keyword only arguments. This means that for info(msg, *args, **kwargs)
you will see neither args
, nor kwargs
.
Note also, that if you add log
, its first argument is level
which should probably not be considered an input.
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.
Pr updated.
override predicate isSanitizer(DataFlow::Node node) { | ||
exists(CallNode call | | ||
node.asCfgNode() = call.getFunction().(AttrNode).getObject("replace") and | ||
call.getArg(0).getNode().(StrConst).getText() in ["\r\n", "\n"] | ||
) | ||
} |
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 slightly unsafe as it deems either replacement ("\r\n" or "\n") a sanitizer when really both are required.
I am not sure how to improve that easily, though. Perhaps we will turn it into a concept with a single implementation and improve it over time, but I am not sure you need to do that now.
Further, this seems overly specific, so I would expect some false positives from this. Consider adding at least StringConstCompare
as a sanitizer option. (Either directly here in this predicate, or by a construction similar to here.)
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.
Do you have a specific case here? Because the safety laboratory did not give me advice on the elimination of FP.
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.
If the user does replace("\r\n", "")
, then that will match your predicate and be considered a sanitizer. However, that would not do any replacements on a message having only "\n"'s in it.
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.
The other comment is that if the message is compared against a known string, that should be considered a sanitizer (since that is much stronger than doing a replacement).
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 think it would be better if you give me the actual case.
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.
@yoff Excuse me again. Do you have examples in actual open source projects for me?
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.
Hm, I had to do a bit of digging. My comment came from seeing code like this, although in that particular case we do want to alert (because the logged value is outside the known set). I think I have seen also that only values in a known set is logged, which would be safe, and that is the sanitizer I proposed. However, it does not seem to be very common, so we could also add it once we see the first FP...
Thanks for the review, about this suggestion, can you describe it again? I am not too understanding. |
I meant that to find html-based log injection, you would need sinks to be logs displayed as HTML and sanitizers to be HTML-relevant escaping. On the whole that seems to be an entirely separate taint tracking configuration. I wonder if that is out of scope for now, but it is mentioned in the help file. |
Thank you for your reply. I am not going to add HTML injection due to log injection in this PR. I will create a new PR to complete this query. |
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 tried to clarify my points.
override DataFlow::Node getAnInput() { | ||
this.getFunction().(DataFlow::AttrRead).getAttributeName() != "log" and | ||
result = this.getArg(0) | ||
or | ||
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "log" and | ||
result = this.getArg(1) | ||
} |
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.
override DataFlow::Node getAnInput() { | |
this.getFunction().(DataFlow::AttrRead).getAttributeName() != "log" and | |
result = this.getArg(0) | |
or | |
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "log" and | |
result = this.getArg(1) | |
} | |
override DataFlow::Node getAnInput() { | |
this.getFunction().(DataFlow::AttrRead).getAttributeName() != "log" and | |
result in [this.getArg(_), this.getArgByName(_) ] // this includes the arg named "msg" | |
or | |
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "log" and | |
result in [this.getArg(any(int i | i > 0)), this.getArgByName(any(string s | s != "level")] | |
} |
I think my previous comment was a bit unclear, it should be something like the above.
If you have a signature like
warning(msg, *args, **kwargs)
you can call it with
warning("msg", "arg1", "arg2", kwarg1="kwarg1", kwargs="kwarg2")
and all of these arguments seem to end up in the log.
override predicate isSanitizer(DataFlow::Node node) { | ||
exists(CallNode call | | ||
node.asCfgNode() = call.getFunction().(AttrNode).getObject("replace") and | ||
call.getArg(0).getNode().(StrConst).getText() in ["\r\n", "\n"] | ||
) | ||
} |
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.
If the user does replace("\r\n", "")
, then that will match your predicate and be considered a sanitizer. However, that would not do any replacements on a message having only "\n"'s in it.
override predicate isSanitizer(DataFlow::Node node) { | ||
exists(CallNode call | | ||
node.asCfgNode() = call.getFunction().(AttrNode).getObject("replace") and | ||
call.getArg(0).getNode().(StrConst).getText() in ["\r\n", "\n"] | ||
) | ||
} |
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.
The other comment is that if the message is compared against a known string, that should be considered a sanitizer (since that is much stronger than doing a replacement).
@yoff Compared with the known string, is the known string "\r\n" or "\n"? |
Friendly ping. |
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 still vary of the FP rate, but I do not have any evidence for it, so happy to approve for now.
Thank you for your reply. If you have any suggestions, please let me know and I will improve the configuration query. |
The language tests fail because we now have a mechanism for computing subpaths. You can merge from main and rerun tests. I will also make a suggestion, that I think will make them pass.. |
python/ql/test/experimental/query-tests/Security/CWE-117/LogInjection.expected
Show resolved
Hide resolved
…jection.expected Co-authored-by: yoff <lerchedahl@gmail.com>
The final error is due to formatting, particularly |
@yoff Automatic formatting is done, please review whether it is passed, thank you. |
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.
LGTM
If unsanitized user input is written to a log entry, a malicious user may be able to forge new log entries.