Compare commits

...

4 Commits

Author SHA1 Message Date
Harry Maclean
150a0e569f Ruby: Add basic modeling for ViewComponent 2024-01-17 10:51:32 +00:00
Harry Maclean
6afeb1d2f9 Update expected file 2024-01-16 11:39:43 +00:00
Harry Maclean
173e1def4f Add WIP query for erb flow 2024-01-16 11:36:39 +00:00
Harry Maclean
2af00a0be9 Add test for erb flow 2024-01-04 12:31:13 +00:00
7 changed files with 131 additions and 0 deletions

View File

@@ -38,3 +38,4 @@ private import codeql.ruby.frameworks.Yaml
private import codeql.ruby.frameworks.Sequel
private import codeql.ruby.frameworks.Ldap
private import codeql.ruby.frameworks.Jwt
private import codeql.ruby.frameworks.ViewComponent

View File

@@ -0,0 +1,41 @@
private import codeql.ruby.AST
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.SSA
private import codeql.ruby.dataflow.internal.DataFlowPrivate as DataFlowPrivate
/**
* Provides modeling for the `view_component` gem.
*/
module ViewComponent {
/**
* A subclass of `ViewComponent::Base`.
*/
class ComponentClass extends DataFlow::ClassNode {
ComponentClass() {
this = DataFlow::getConstant("ViewComponent").getConstant("Base").getADescendentModule()
}
/**
* Returns the template file for this component.
*/
ErbFile getTemplate() {
result.getAbsolutePath() =
this.getLocation().getFile().getAbsolutePath().replaceAll(".rb", ".html.erb")
}
}
/**
* An additional jump step from a `ComponentClass` passed as an argument in a call to `render`
* to the `self` variable in its corresponding template.
*/
private predicate jumpStep(DataFlow::Node node1, DataFlowPrivate::SsaSelfDefinitionNode node2) {
exists(DataFlow::CallNode call, ComponentClass component |
call.getMethodName() = "render" and
call.getArgument(0) = node1 and
component.trackInstance().getAValueReachableFromSource() = node1 and
node2.getLocation().getFile() = component.getTemplate() and
node2.getSelfScope() instanceof Toplevel and
node2.getDefinitionExt() instanceof Ssa::SelfDefinition
)
}
}

View File

@@ -0,0 +1,16 @@
| main.rb:3:13:3:21 | call to source | main.rb:3:9:3:9 | x |
| main.rb:3:13:3:21 | call to source | main.rb:3:9:3:21 | ... = ... |
| main.rb:3:13:3:21 | call to source | main.rb:4:9:4:12 | view [@x] |
| main.rb:3:13:3:21 | call to source | main.rb:4:9:4:26 | ... = ... [@x] |
| main.rb:3:13:3:21 | call to source | main.rb:4:16:4:26 | call to new [@x] |
| main.rb:3:13:3:21 | call to source | main.rb:4:16:4:26 | synthetic splat argument [splat position 0] |
| main.rb:3:13:3:21 | call to source | main.rb:4:25:4:25 | x |
| main.rb:3:13:3:21 | call to source | main.rb:5:9:5:20 | synthetic splat argument [splat position 0, ... (2)] |
| main.rb:3:13:3:21 | call to source | main.rb:5:16:5:19 | view [@x] |
| main.rb:3:13:3:21 | call to source | main.rb:5:16:5:19 | view [Ext] [@x] |
| main.rb:3:13:3:21 | call to source | view.rb:2:5:4:7 | synthetic splat parameter [splat position 0] |
| main.rb:3:13:3:21 | call to source | view.rb:2:20:2:20 | x |
| main.rb:3:13:3:21 | call to source | view.rb:2:20:2:20 | x |
| main.rb:3:13:3:21 | call to source | view.rb:3:9:3:10 | [post] self [@x] |
| main.rb:3:13:3:21 | call to source | view.rb:3:9:3:14 | ... = ... |
| main.rb:3:13:3:21 | call to source | view.rb:3:14:3:14 | x |

View File

@@ -0,0 +1,56 @@
import codeql.ruby.AST
import codeql.ruby.CFG
import ruby
import codeql.ruby.DataFlow
import codeql.ruby.AST
import codeql.ruby.TaintTracking
import codeql.ruby.frameworks.data.internal.ApiGraphModels
import codeql.ruby.ApiGraphs
import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.CFG
private import codeql.ruby.dataflow.BarrierGuards
import codeql.ruby.ast.internal.Module
import codeql.ruby.dataflow.internal.DataFlowPrivate
import codeql.ruby.dataflow.SSA
from ErbFlow::PartialPathNode source, ErbFlow::PartialPathNode sink
where ErbFlow::partialFlow(source, sink, _)
select source, sink
module ErbFlow = TaintTracking::Global<Erb>::FlowExplorationFwd<explorationLimit/0>;
module Erb implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node node) { node.(DataFlow::CallNode).getMethodName() = "source" }
predicate isSink(DataFlow::Node node) {
node = any(DataFlow::CallNode c | c.getMethodName() = "sink").getArgument(_)
}
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
isFlowFromViewSelfToTemplate(node1, node2)
}
}
int explorationLimit() { result = 10 }
predicate isFlowFromViewSelfToTemplate(DataFlow::Node node1, SsaSelfDefinitionNode node2) {
exists(DataFlow::CallNode call, DataFlow::ClassNode view |
call.getMethodName() = "render" and
call.getArgument(0) = node1 and
view.trackInstance().getAValueReachableFromSource() = node1 and
exists(ErbFile template |
view = getTemplateAssociatedViewClass(template) and node2.getLocation().getFile() = template
) and
node2.getSelfScope() instanceof Toplevel and
node2.getDefinitionExt() instanceof Ssa::SelfDefinition
)
}
DataFlow::ClassNode getTemplateAssociatedViewClass(ErbFile template) {
// template is in same directory as view
exists(File viewFile | viewFile = result.getADeclaration().getFile() |
template.getParentContainer().getAbsolutePath() =
viewFile.getParentContainer().getAbsolutePath() and
viewFile.getStem() = template.getStem()
)
}

View File

@@ -0,0 +1,7 @@
class App
def run
x = source(1)
view = View.new(x)
render(view)
end
end

View File

@@ -0,0 +1 @@
<%= foo() %>

View File

@@ -0,0 +1,9 @@
class View
def initialize(x)
@x = x
end
def foo
sink(@x) # $ hasValueFlow=1
end
end