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

Rust: Query for cleartext logging of sensitive information #18582

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion rust/ql/lib/codeql/rust/dataflow/internal/DataFlowImpl.qll
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ final class ArrayElementContent extends Content, TArrayElement {
* NOTE: Unlike `struct`s and `enum`s tuples are structural and not nominal,
* hence we don't store a canonical path for them.
*/
private class TuplePositionContent extends Content, TTuplePositionContent {
final class TuplePositionContent extends Content, TTuplePositionContent {
private int pos;

TuplePositionContent() { this = TTuplePositionContent(pos) }
Expand Down
17 changes: 17 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/log.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
extensions:
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["repo:https://github.com/rust-lang/log:log", "crate::__private_api::log", "Argument[0]", "log-injection", "manual"] # args
- ["repo:https://github.com/rust-lang/log:log", "crate::__private_api::log", "Argument[2]", "log-injection", "manual"] # target
- ["repo:https://github.com/rust-lang/log:log", "crate::__private_api::log", "Argument[3]", "log-injection", "manual"] # key value
- ["lang:std", "crate::io::stdio::_print", "Argument[0]", "log-injection", "manual"]
- ["lang:std", "crate::io::stdio::_eprint", "Argument[0]", "log-injection", "manual"]
- ["lang:std", "<crate::io::stdio::StdoutLock as crate::io::Write>::write", "Argument[0]", "log-injection", "manual"]
- ["lang:std", "<crate::io::stdio::StdoutLock as crate::io::Write>::write_all", "Argument[0]", "log-injection", "manual"]
- ["lang:std", "<crate::io::stdio::StderrLock as crate::io::Write>::write", "Argument[0]", "log-injection", "manual"]
- ["lang:std", "<crate::io::stdio::StderrLock as crate::io::Write>::write_all", "Argument[0]", "log-injection", "manual"]
- ["lang:core", "crate::panicking::panic_fmt", "Argument[0]", "log-injection", "manual"]
- ["lang:core", "crate::panicking::assert_failed", "Argument[3].Variant[crate::option::Option::Some(0)]", "log-injection", "manual"]
- ["lang:core", "<crate::option::Option>::expect", "Argument[0]", "log-injection", "manual"]
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extensions:
- ["lang:core", "<crate::result::Result>::unwrap_or", "Argument[0]", "ReturnValue", "value", "manual"]
# String
- ["lang:alloc", "<crate::string::String>::as_str", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["lang:alloc", "<crate::string::String>::as_bytes", "Argument[self]", "ReturnValue", "taint", "manual"]
# Hint
- ["lang:core", "crate::hint::must_use", "Argument[0]", "ReturnValue", "value", "manual"]
# Fmt
Expand Down
38 changes: 38 additions & 0 deletions rust/ql/src/queries/security/CWE-312/CleartextLogging.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>
Sensitive user data and system information that is logged could be seen by an attacker when it is
displayed. Also, external processes often store the standard output and standard error streams of
an application, which will include logged sensitive information.</p>
</p>
</overview>

<recommendation>
<p>
Do not log sensitive data. If it is necessary to log sensitive data, encrypt it before logging.
</p>
</recommendation>

<example>
<p>
The following example code logs user credentials (in this case, their password) in plaintext:
</p>
<sample src="CleartextLoggingBad.rs"/>
<p>
Instead, you should encrypt the credentials, or better still omit them entirely:
</p>
<sample src="CleartextLoggingGood.rs"/>
</example>

<references>

<li>M. Dowd, J. McDonald and J. Schuhm, <i>The Art of Software Security Assessment</i>, 1st Edition, Chapter 2 - 'Common Vulnerabilities of Encryption', p. 43. Addison Wesley, 2006.</li>
<li>M. Howard and D. LeBlanc, <i>Writing Secure Code</i>, 2nd Edition, Chapter 9 - 'Protecting Secret Data', p. 299. Microsoft, 2002.</li>
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html#data-to-exclude">Logging Cheat Sheet - Data to exclude</a>.<li>

</references>
</qhelp>
58 changes: 58 additions & 0 deletions rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* @name Cleartext logging of sensitive information
* @description Logging sensitive information in plaintext can
* expose it to an attacker.
* @kind path-problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id rust/cleartext-logging
* @tags security
* external/cwe/cwe-312
* external/cwe/cwe-359
* external/cwe/cwe-532
*/

import rust
import codeql.rust.security.CleartextLoggingExtensions
import codeql.rust.dataflow.DataFlow
import codeql.rust.dataflow.TaintTracking
import codeql.rust.dataflow.internal.DataFlowImpl

/**
* A taint-tracking configuration for cleartext logging vulnerabilities.
*/
module CleartextLoggingConfig implements DataFlow::ConfigSig {
import CleartextLogging

predicate isSource(DataFlow::Node source) { source instanceof Source }

predicate isSink(DataFlow::Node sink) { sink instanceof Sink }

predicate isBarrier(DataFlow::Node barrier) { barrier instanceof Barrier }

predicate isBarrierIn(DataFlow::Node node) {
// make sources barriers so that we only report the closest instance
isSource(node)
}

predicate isAdditionalFlowStep(Node node1, Node node2) {
// flow from `a` to `&a`
node2.asExpr().getExpr().(RefExpr).getExpr() = node1.asExpr().getExpr()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We possibly want something like this as a general flow step. It matters for basically all the test cases that mention &password in the source I think. @paldepind what do you think?

}

predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) {
// flow out from tuple content at sinks.
isSink(node) and
c.getAReadContent() instanceof TuplePositionContent
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 couldn't get tuple content to work in the models-as-data sink itself, so this might have to do for now.

}
}

module CleartextLoggingFlow = TaintTracking::Global<CleartextLoggingConfig>;

import CleartextLoggingFlow::PathGraph

from CleartextLoggingFlow::PathNode source, CleartextLoggingFlow::PathNode sink
where CleartextLoggingFlow::flowPath(source, sink)
select sink.getNode(), source, sink, "This operation writes $@ to a log file.", source,
source.getNode().toString()
2 changes: 2 additions & 0 deletions rust/ql/src/queries/security/CWE-312/CleartextLoggingBad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let password = "P@ssw0rd"
geoffw0 marked this conversation as resolved.
Show resolved Hide resolved
info!("User password changed to {password}")
2 changes: 2 additions & 0 deletions rust/ql/src/queries/security/CWE-312/CleartextLoggingGood.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let password = "P@ssw0rd"
info!("User password changed")
5 changes: 4 additions & 1 deletion rust/ql/src/queries/summary/Stats.qll
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ private import codeql.rust.AstConsistency as AstConsistency
private import codeql.rust.controlflow.internal.CfgConsistency as CfgConsistency
private import codeql.rust.dataflow.internal.DataFlowConsistency as DataFlowConsistency
private import codeql.rust.security.SqlInjectionExtensions
private import codeql.rust.security.CleartextLoggingExtensions

/**
* Gets a count of the total number of lines of code in the database.
Expand Down Expand Up @@ -58,7 +59,9 @@ int getTaintEdgesCount() {
* Gets a kind of query for which `n` is a sink (if any).
*/
string getAQuerySinkKind(DataFlow::Node n) {
(n instanceof SqlInjection::Sink and result = "SqlInjection")
n instanceof SqlInjection::Sink and result = "SqlInjection"
or
n instanceof CleartextLogging::Sink and result = "CleartextLogging"
}

/**
Expand Down
4 changes: 2 additions & 2 deletions rust/ql/test/query-tests/diagnostics/SummaryStats.expected
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
| Macro calls - resolved | 8 |
| Macro calls - total | 9 |
| Macro calls - unresolved | 1 |
| Taint edges - number of edges | 2 |
| Taint edges - number of edges | 3 |
| Taint reach - nodes tainted | 0 |
| Taint reach - per million nodes | 0 |
| Taint sinks - cryptographic operations | 0 |
| Taint sinks - query sinks | 0 |
| Taint sinks - query sinks | 3 |
| Taint sources - active | 0 |
| Taint sources - disabled | 0 |
| Taint sources - sensitive data | 0 |
Loading
Loading