implement basic map get/set for immutable.js

This commit is contained in:
Erik Krogh Kristensen
2021-02-03 19:24:27 +01:00
parent 26288ad391
commit b77dd54618
6 changed files with 146 additions and 1 deletions

View File

@@ -85,6 +85,7 @@ import semmle.javascript.frameworks.Electron
import semmle.javascript.frameworks.EventEmitter
import semmle.javascript.frameworks.Files
import semmle.javascript.frameworks.Firebase
import semmle.javascript.frameworks.Immutable
import semmle.javascript.frameworks.jQuery
import semmle.javascript.frameworks.JWT
import semmle.javascript.frameworks.Handlebars

View File

@@ -659,9 +659,15 @@ module PseudoProperties {
*/
pragma[inline]
string mapValueKnownKey(DataFlow::Node key) {
result = pseudoProperty("mapValue", any(string s | key.mayHaveStringValue(s)))
result = mapValueKey(any(string s | key.mayHaveStringValue(s)))
}
/**
* Gets a pseudo-property for the location of a map value where the key is `key`.
*/
bindingset[key]
string mapValueKey(string key) { result = pseudoProperty("mapValue", key) }
/**
* Gets a pseudo-property for the location of a map value where the key is `key`.
*/

View File

@@ -0,0 +1,100 @@
/**
* Provides classes and predicates for reasoning about [immutable](https://www.npmjs.com/package/immutable).
*/
import javascript
/**
* Provides classes implementing data-flow for Immutable.
*/
private module Immutable {
private import DataFlow::PseudoProperties
/**
* An API entrypoint for the global `Immutable` variable.
*/
private class ImmutableGlobalEntry extends API::EntryPoint {
ImmutableGlobalEntry() { this = "ImmutableGlobalEntry" }
override DataFlow::SourceNode getAUse() { result = DataFlow::globalVarRef("Immutable") }
override DataFlow::Node getARhs() { none() }
}
/**
* An import of the `Immutable` library.
*/
API::Node immutableImport() {
result = API::moduleImport("immutable")
or
result = API::root().getASuccessor(any(ImmutableGlobalEntry i))
}
/**
* An instance of an immutable map.
*/
API::Node immutableMap() {
result = immutableImport().getMember("Map").getReturn()
or
result = immutableMap().getMember("set").getReturn()
}
/**
* Gets the immutable collection where `pred` has been stored using the pseudoproperty `prop`.
*/
DataFlow::SourceNode storeStep(DataFlow::Node pred, string prop) {
exists(DataFlow::CallNode call, string key |
call = immutableImport().getMember("Map").getACall()
|
prop = mapValueKey(key) and
pred = call.getOptionArgument(0, key) and
result = call
)
// TODO: map set.
}
/**
* Gets the value that was stored in the immutable collection `pred` under the pseudoproperty `prop`.
*/
DataFlow::Node loadStep(DataFlow::Node pred, string prop) {
// map.get()
exists(DataFlow::MethodCallNode call | call = immutableMap().getMember("get").getACall() |
pred = call.getReceiver() and
result = call and
prop = mapValue(call.getArgument(0))
)
}
/**
* Gets an immutable collection that contains all the elements from `pred`.
*/
DataFlow::Node step(DataFlow::Node pred) {
// map.set() copies all existing values
exists(DataFlow::CallNode call | call = immutableMap().getMember("set").getACall() |
pred = call.getReceiver() and
result = call
)
}
/**
* A dataflow step for an immutable collection.
*/
class ImmutableConstructionStep extends DataFlow::AdditionalFlowStep {
ImmutableConstructionStep() { this = [loadStep(_, _), storeStep(_, _), step(_)] }
override predicate loadStep(DataFlow::Node pred, DataFlow::Node succ, string prop) {
this = loadStep(pred, prop) and
succ = this
}
override predicate storeStep(DataFlow::Node pred, DataFlow::SourceNode succ, string prop) {
this = storeStep(pred, prop) and
succ = this
}
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
this = step(pred) and
succ = this
}
}
}

View File

@@ -0,0 +1,15 @@
var obj = { a: source("a"), b: source("b1") };
sink(obj["a"]); // NOT OK
const { Map } = require('immutable');
const map1 = Map(obj);
sink(map1.get("b")); // NOT OK
const map2 = map1.set('c', "safe");
sink(map1.get("a")); // NOT OK
sink(map2.get("a")); // NOT OK
sink(map2.get("b")); // OK - but still flagged [INCONSISTENCY]

View File

@@ -0,0 +1,5 @@
| immutable.js:1:16:1:26 | source("a") | immutable.js:2:6:2:13 | obj["a"] |
| immutable.js:1:16:1:26 | source("a") | immutable.js:11:6:11:18 | map1.get("a") |
| immutable.js:1:16:1:26 | source("a") | immutable.js:12:6:12:18 | map2.get("a") |
| immutable.js:1:32:1:43 | source("b1") | immutable.js:8:6:8:18 | map1.get("b") |
| immutable.js:1:32:1:43 | source("b1") | immutable.js:13:6:13:18 | map2.get("b") |

View File

@@ -0,0 +1,18 @@
import javascript
private import semmle.javascript.dataflow.internal.StepSummary
class Config extends DataFlow::Configuration {
Config() { this = "Config" }
override predicate isSource(DataFlow::Node source) {
source.(DataFlow::CallNode).getCalleeName() = "source"
}
override predicate isSink(DataFlow::Node sink) {
exists(DataFlow::CallNode call | call.getCalleeName() = "sink" | call.getAnArgument() = sink)
}
}
query predicate dataFlow(DataFlow::Node pred, DataFlow::Node succ) {
any(Config c).hasFlow(pred, succ)
}