Skip to content

Commit 5fa6ecc

Browse files
committed
Ruby: Model what is written to the log from stdlib Logger methods
1 parent 861ae85 commit 5fa6ecc

File tree

5 files changed

+233
-0
lines changed

5 files changed

+233
-0
lines changed

ruby/ql/lib/codeql/ruby/Concepts.qll

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,3 +644,32 @@ module Path {
644644
abstract class Range extends DataFlow::Node { }
645645
}
646646
}
647+
648+
/**
649+
* A data-flow node that logs data.
650+
*
651+
* Extend this class to refine existing API models. If you want to model new APIs,
652+
* extend `Logging::Range` instead.
653+
*/
654+
class Logging extends DataFlow::Node {
655+
Logging::Range range;
656+
657+
Logging() { this = range }
658+
659+
/** Gets an input that is logged. */
660+
DataFlow::Node getAnInput() { result = range.getAnInput() }
661+
}
662+
663+
/** Provides a class for modeling new logging mechanisms. */
664+
module Logging {
665+
/**
666+
* A data-flow node that logs data.
667+
*
668+
* Extend this class to model new APIs. If you want to refine existing API models,
669+
* extend `Logging` instead.
670+
*/
671+
abstract class Range extends DataFlow::Node {
672+
/** Gets an input that is logged. */
673+
abstract DataFlow::Node getAnInput();
674+
}
675+
}

ruby/ql/lib/codeql/ruby/frameworks/StandardLibrary.qll

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ private import codeql.ruby.Concepts
33
private import codeql.ruby.DataFlow
44
private import codeql.ruby.ApiGraphs
55
private import codeql.ruby.dataflow.FlowSummary
6+
private import codeql.ruby.dataflow.internal.DataFlowDispatch
7+
private import codeql.ruby.CFG
68

79
/**
810
* The `Kernel` module is included by the `Object` class, so its methods are available
@@ -347,3 +349,103 @@ class RegexpEscapeSummary extends SummarizedCallable {
347349
preservesValue = false
348350
}
349351
}
352+
353+
/** A reference to a `Logger` instance */
354+
private DataFlow::Node loggerInstance() {
355+
result = API::getTopLevelMember("Logger").getAnInstantiation()
356+
or
357+
exists(DataFlow::Node inst |
358+
inst = loggerInstance() and
359+
inst.(DataFlow::LocalSourceNode).flowsTo(result)
360+
)
361+
or
362+
// Assume that a variable assigned as a `Logger` instance is always a
363+
// `Logger` instance. This covers class and instance variables where we can't
364+
// necessarily trace a dataflow path from assignment to use.
365+
exists(Variable v, Assignment a |
366+
a.getLeftOperand().getAVariable() = v and
367+
a.getRightOperand() = loggerInstance().asExpr().getExpr() and
368+
result.asExpr().getExpr().(VariableReadAccess).getVariable() = v
369+
)
370+
}
371+
372+
/**
373+
* A call to a `Logger` instance method that causes a message to be logged.
374+
*/
375+
abstract class LoggerLoggingCall extends Logging::Range, DataFlow::CallNode {
376+
LoggerLoggingCall() { this.getReceiver() = loggerInstance() }
377+
}
378+
379+
/**
380+
* A call to `Logger#add` or its alias `Logger#log`.
381+
*/
382+
private class LoggerAddCall extends LoggerLoggingCall {
383+
LoggerAddCall() { this.getMethodName() = ["add", "log"] }
384+
385+
override DataFlow::Node getAnInput() {
386+
// Both the message and the progname are form part of the log output:
387+
// Logger#add(severity, message) / Logger#add(severity, message, progname)
388+
result = this.getArgument(1)
389+
or
390+
result = this.getArgument(2)
391+
or
392+
// a return value from the block in Logger#add(severity) <block> or in
393+
// Logger#add(severity, nil, progname) <block>
394+
(
395+
this.getNumberOfArguments() = 1
396+
or
397+
// TODO: this could track the value of the `message` argument to make
398+
// this check more accurate
399+
this.getArgument(1).asExpr().getExpr() instanceof NilLiteral
400+
) and
401+
exprNodeReturnedFrom(result, this.getBlock().asExpr().getExpr())
402+
}
403+
}
404+
405+
/**
406+
* A call to `Logger#<<`.
407+
*/
408+
private class LoggerPushCall extends LoggerLoggingCall {
409+
LoggerPushCall() { this.getMethodName() = "<<" }
410+
411+
override DataFlow::Node getAnInput() {
412+
// Logger#<<(msg)
413+
result = this.getArgument(0)
414+
}
415+
}
416+
417+
/**
418+
* A call to a `Logger` method that logs at a preset severity level.
419+
*
420+
* Specifically, these methods are `debug`, `error`, `fatal`, `info`,
421+
* `unknown`, and `warn`.
422+
*/
423+
private class LoggerInfoStyleCall extends LoggerLoggingCall {
424+
LoggerInfoStyleCall() {
425+
this.getMethodName() = ["debug", "error", "fatal", "info", "unknown", "warn"]
426+
}
427+
428+
override DataFlow::Node getAnInput() {
429+
// `msg` from `Logger#info(msg)`,
430+
// or `progname` from `Logger#info(progname) <block>`
431+
result = this.getArgument(0)
432+
or
433+
// a return value from the block in `Logger#info(progname) <block>`
434+
exprNodeReturnedFrom(result, this.getBlock().asExpr().getExpr())
435+
}
436+
}
437+
438+
/**
439+
* A call to `Logger#progname=`. This sets a default progname.
440+
* This call does not log anything directly, but the assigned value can appear
441+
* in future log messages that do not specify a `progname` argument.
442+
*/
443+
private class LoggerSetPrognameCall extends LoggerLoggingCall {
444+
LoggerSetPrognameCall() { this.getMethodName() = "progname=" }
445+
446+
override DataFlow::Node getAnInput() {
447+
exists(CfgNodes::ExprNodes::AssignExprCfgNode a | this.getArgument(0).asExpr() = a |
448+
result.asExpr() = a.getRhs()
449+
)
450+
}
451+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
require 'logger'
2+
3+
class LoggerTest
4+
@@cls_logger = Logger.new STDERR
5+
@@cls_logger.progname = "LoggerTest"
6+
7+
def init_logger
8+
if @logger == nil
9+
@logger = Logger.new STDOUT
10+
end
11+
end
12+
13+
def debug_log(msg)
14+
init_logger
15+
@logger.debug msg
16+
end
17+
18+
def error_log(msg)
19+
init_logger
20+
@logger.error do
21+
msg + "!"
22+
end
23+
end
24+
25+
def fatal_log(msg)
26+
init_logger
27+
@logger.fatal msg
28+
end
29+
30+
def warn_log(msg)
31+
init_logger
32+
@logger.warn msg
33+
end
34+
35+
def unknown_log(msg)
36+
init_logger
37+
@logger.unknown("unknown prog") { msg }
38+
end
39+
40+
def info_log(msg)
41+
init_logger
42+
@logger.info do
43+
if msg.size > 100
44+
msg[0..91] + "..." + msg[-5..msg.size]
45+
else
46+
msg
47+
end
48+
end
49+
end
50+
51+
def push_log(msg)
52+
logger = Logger.new STDERR
53+
logger_alias = logger
54+
logger_alias << ("test message: " + msg)
55+
end
56+
57+
def add_log(msg)
58+
@@cls_logger.add(Logger::INFO) { "block" }
59+
# Includes both progname and block return if 'message' is 'nil'
60+
@@cls_logger.add(Logger::INFO, nil, "progname1") { "block" }
61+
62+
# block return value is ignored if `message` is specified
63+
@@cls_logger.add(Logger::WARN, "message1") { "not logged" }
64+
@@cls_logger.add(Logger::WARN, "message2", "progname2") { "not logged" }
65+
end
66+
67+
def log_log(msg)
68+
@@cls_logger.log(Logger::INFO) { "block" }
69+
# Includes both progname and block return if 'message' is 'nil'
70+
@@cls_logger.log(Logger::INFO, nil, "progname1") { "block" }
71+
72+
# block return value is ignored if `message` is specified
73+
@@cls_logger.log(Logger::WARN, "message1") { "not logged" }
74+
@@cls_logger.log(Logger::WARN, "message2", "progname2") { "not logged" }
75+
end
76+
end

ruby/ql/test/library-tests/frameworks/StandardLibrary.expected

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,27 @@ classEvalCallCodeExecutions
6969
| Eval.rb:25:1:25:47 | call to class_eval | Eval.rb:25:16:25:32 | "def foo; 1; end" |
7070
moduleEvalCallCodeExecutions
7171
| Eval.rb:26:1:26:54 | call to module_eval | Eval.rb:26:17:26:33 | "def bar; 1; end" |
72+
loggerLoggingCallInputs
73+
| Logging.rb:5:3:5:23 | call to progname= | Logging.rb:5:27:5:38 | "LoggerTest" |
74+
| Logging.rb:15:5:15:21 | call to debug | Logging.rb:15:19:15:21 | msg |
75+
| Logging.rb:20:5:22:7 | call to error | Logging.rb:21:7:21:15 | ... + ... |
76+
| Logging.rb:27:5:27:21 | call to fatal | Logging.rb:27:19:27:21 | msg |
77+
| Logging.rb:32:5:32:20 | call to warn | Logging.rb:32:18:32:20 | msg |
78+
| Logging.rb:37:5:37:43 | call to unknown | Logging.rb:37:21:37:34 | "unknown prog" |
79+
| Logging.rb:37:5:37:43 | call to unknown | Logging.rb:37:39:37:41 | msg |
80+
| Logging.rb:42:5:48:7 | call to info | Logging.rb:43:7:47:9 | if ... |
81+
| Logging.rb:54:5:54:44 | ... << ... | Logging.rb:54:21:54:44 | ( ... ) |
82+
| Logging.rb:58:5:58:46 | call to add | Logging.rb:58:38:58:44 | "block" |
83+
| Logging.rb:60:5:60:64 | call to add | Logging.rb:60:36:60:38 | nil |
84+
| Logging.rb:60:5:60:64 | call to add | Logging.rb:60:41:60:51 | "progname1" |
85+
| Logging.rb:60:5:60:64 | call to add | Logging.rb:60:56:60:62 | "block" |
86+
| Logging.rb:63:5:63:63 | call to add | Logging.rb:63:36:63:45 | "message1" |
87+
| Logging.rb:64:5:64:76 | call to add | Logging.rb:64:36:64:45 | "message2" |
88+
| Logging.rb:64:5:64:76 | call to add | Logging.rb:64:48:64:58 | "progname2" |
89+
| Logging.rb:68:5:68:46 | call to log | Logging.rb:68:38:68:44 | "block" |
90+
| Logging.rb:70:5:70:64 | call to log | Logging.rb:70:36:70:38 | nil |
91+
| Logging.rb:70:5:70:64 | call to log | Logging.rb:70:41:70:51 | "progname1" |
92+
| Logging.rb:70:5:70:64 | call to log | Logging.rb:70:56:70:62 | "block" |
93+
| Logging.rb:73:5:73:63 | call to log | Logging.rb:73:36:73:45 | "message1" |
94+
| Logging.rb:74:5:74:76 | call to log | Logging.rb:74:36:74:45 | "message2" |
95+
| Logging.rb:74:5:74:76 | call to log | Logging.rb:74:48:74:58 | "progname2" |

ruby/ql/test/library-tests/frameworks/StandardLibrary.ql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,5 @@ query DataFlow::Node classEvalCallCodeExecutions(ClassEvalCallCodeExecution e) {
3030
query DataFlow::Node moduleEvalCallCodeExecutions(ModuleEvalCallCodeExecution e) {
3131
result = e.getCode()
3232
}
33+
34+
query DataFlow::Node loggerLoggingCallInputs(LoggerLoggingCall c) { result = c.getAnInput() }

0 commit comments

Comments
 (0)