mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
Merge pull request #705 from asger-semmle/loop-index-concurrent-modification
Approved by mc-semmle, xiemaisi
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
| Incomplete regular expression for hostnames (`js/incomplete-hostname-regexp`) | correctness, security, external/cwe/cwe-020 | Highlights hostname sanitizers that are likely to be incomplete, indicating a violation of [CWE-020](https://cwe.mitre.org/data/definitions/20.html). Results are shown on LGTM by default.|
|
||||
| Incomplete URL substring sanitization | correctness, security, external/cwe/cwe-020 | Highlights URL sanitizers that are likely to be incomplete, indicating a violation of [CWE-020](https://cwe.mitre.org/data/definitions/20.html). Results shown on LGTM by default. |
|
||||
| Incorrect suffix check (`js/incorrect-suffix-check`) | correctness, security, external/cwe/cwe-020 | Highlights error-prone suffix checks based on `indexOf`, indicating a potential violation of [CWE-20](https://cwe.mitre.org/data/definitions/20.html). Results are shown on LGTM by default. |
|
||||
| Loop iteration skipped due to shifting (`js/loop-iteration-skipped-due-to-shifting`) | correctness | Highlights code that removes an element from an array while iterating over it, causing the loop to skip over some elements. Results are shown on LGTM by default. |
|
||||
| Useless comparison test (`js/useless-comparison-test`) | correctness | Highlights code that is unreachable due to a numeric comparison that is always true or always false. Results are shown on LGTM by default. |
|
||||
|
||||
## Changes to existing queries
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
+ semmlecode-javascript-queries/LanguageFeatures/IllegalInvocation.ql: /Correctness/Language Features
|
||||
+ semmlecode-javascript-queries/LanguageFeatures/InconsistentNew.ql: /Correctness/Language Features
|
||||
+ semmlecode-javascript-queries/LanguageFeatures/SpuriousArguments.ql: /Correctness/Language Features
|
||||
+ semmlecode-javascript-queries/Statements/LoopIterationSkippedDueToShifting.ql: /Correctness/Statements
|
||||
+ semmlecode-javascript-queries/Statements/MisleadingIndentationAfterControlStmt.ql: /Correctness/Statements
|
||||
+ semmlecode-javascript-queries/Statements/ReturnOutsideFunction.ql: /Correctness/Statements
|
||||
+ semmlecode-javascript-queries/Statements/SuspiciousUnusedLoopIterationVariable.ql: /Correctness/Statements
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>
|
||||
Items can be removed from an array using the <code>splice</code> method, but when doing so,
|
||||
all subsequent items will be shifted to a lower index. If this is done while iterating over
|
||||
the array, the shifting may cause the loop to skip over the element immediately after the
|
||||
removed element.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>
|
||||
Determine what the loop is supposed to do:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>
|
||||
If the intention is to remove <em>every occurrence</em> of a certain value, decrement the loop counter after removing an element, to counterbalance
|
||||
the shift.
|
||||
</li>
|
||||
<li>
|
||||
If the loop is only intended to remove <em>a single value</em> from the array, consider adding a <code>break</code> after the <code>splice</code> call.
|
||||
</li>
|
||||
<li>
|
||||
If the loop is deliberately skipping over elements, consider moving the index increment into the body of the loop,
|
||||
so it is clear that the loop is not a trivial array iteration loop.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</recommendation>
|
||||
<example>
|
||||
|
||||
<p>
|
||||
In this example, a function is intended to remove "<code>..</code>" parts from a path:
|
||||
</p>
|
||||
|
||||
<sample src="examples/LoopIterationSkippedDueToShifting.js" />
|
||||
|
||||
<p>
|
||||
However, whenever the input contain two "<code>..</code>" parts right after one another, only the first will be removed.
|
||||
For example, the string "<code>../../secret.txt</code>" will be mapped to "<code>../secret.txt</code>". After removing
|
||||
the element at index 0, the loop counter is incremented to 1, but the second "<code>..</code>" string has now been shifted down to
|
||||
index 0 and will therefore be skipped.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
One way to avoid this is to decrement the loop counter after removing an element from the array:
|
||||
</p>
|
||||
|
||||
<sample src="examples/LoopIterationSkippedDueToShiftingGood.js" />
|
||||
|
||||
<p>
|
||||
Alternatively, use the <code>filter</code> method:
|
||||
</p>
|
||||
|
||||
<sample src="examples/LoopIterationSkippedDueToShiftingGoodFilter.js" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice">Array.prototype.splice()</a>.</li>
|
||||
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter">Array.prototype.filter()</a>.</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* @name Loop iteration skipped due to shifting
|
||||
* @description Removing elements from an array while iterating over it can cause the loop to skip over some elements,
|
||||
* unless the loop index is decremented accordingly.
|
||||
* @kind problem
|
||||
* @problem.severity warning
|
||||
* @id js/loop-iteration-skipped-due-to-shifting
|
||||
* @tags correctness
|
||||
* @precision high
|
||||
*/
|
||||
import javascript
|
||||
|
||||
/**
|
||||
* An operation that inserts or removes elements from an array while shifting all elements
|
||||
* occuring after the insertion/removal point.
|
||||
*
|
||||
* Does not include `push` and `pop` since these never shift any elements.
|
||||
*/
|
||||
class ArrayShiftingCall extends DataFlow::MethodCallNode {
|
||||
string name;
|
||||
|
||||
ArrayShiftingCall() {
|
||||
name = getMethodName() and
|
||||
(name = "splice" or name = "shift" or name = "unshift")
|
||||
}
|
||||
|
||||
DataFlow::SourceNode getArray() {
|
||||
result = getReceiver().getALocalSource()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `splice` on an array.
|
||||
*/
|
||||
class SpliceCall extends ArrayShiftingCall {
|
||||
SpliceCall() {
|
||||
name = "splice"
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index from which elements are removed and possibly new elemenst are inserted.
|
||||
*/
|
||||
DataFlow::Node getIndex() {
|
||||
result = getArgument(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of removed elements.
|
||||
*/
|
||||
int getNumRemovedElements() {
|
||||
result = getArgument(1).asExpr().getIntValue() and
|
||||
result >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of inserted elements.
|
||||
*/
|
||||
int getNumInsertedElements() {
|
||||
result = getNumArgument() - 2 and
|
||||
result >= 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A `for` loop iterating over the indices of an array, in increasing order.
|
||||
*/
|
||||
class ArrayIterationLoop extends ForStmt {
|
||||
DataFlow::SourceNode array;
|
||||
LocalVariable indexVariable;
|
||||
|
||||
ArrayIterationLoop() {
|
||||
exists (RelationalComparison compare | compare = getTest() |
|
||||
compare.getLesserOperand() = indexVariable.getAnAccess() and
|
||||
compare.getGreaterOperand() = array.getAPropertyRead("length").asExpr()) and
|
||||
getUpdate().(IncExpr).getOperand() = indexVariable.getAnAccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the variable holding the loop variable and current array index.
|
||||
*/
|
||||
LocalVariable getIndexVariable() {
|
||||
result = indexVariable
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the loop entry point.
|
||||
*/
|
||||
ReachableBasicBlock getLoopEntry() {
|
||||
result = getTest().getFirstControlFlowNode().getBasicBlock()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call that potentially shifts the elements of the given array.
|
||||
*/
|
||||
ArrayShiftingCall getAnArrayShiftingCall() {
|
||||
result.getArray() = array
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call to `splice` that removes elements from the looped-over array at the current index
|
||||
*
|
||||
* The `splice` call is not guaranteed to be inside the loop body.
|
||||
*/
|
||||
SpliceCall getACandidateSpliceCall() {
|
||||
result = getAnArrayShiftingCall() and
|
||||
result.getIndex().asExpr() = getIndexVariable().getAnAccess() and
|
||||
result.getNumRemovedElements() > result.getNumInsertedElements()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if `cfg` modifies the index variable or shifts array elements, disturbing the
|
||||
* relationship between the array and the index variable.
|
||||
*/
|
||||
predicate hasIndexingManipulation(ControlFlowNode cfg) {
|
||||
cfg.(VarDef).getAVariable() = getIndexVariable() or
|
||||
cfg = getAnArrayShiftingCall().asExpr()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a `loop entry -> cfg` path that does not involve index manipulation or a successful index equality check.
|
||||
*/
|
||||
predicate hasPathTo(ControlFlowNode cfg) {
|
||||
exists(getACandidateSpliceCall()) and // restrict size of predicate
|
||||
cfg = getLoopEntry().getFirstNode()
|
||||
or
|
||||
hasPathTo(cfg.getAPredecessor()) and
|
||||
getLoopEntry().dominates(cfg.getBasicBlock()) and
|
||||
not hasIndexingManipulation(cfg) and
|
||||
|
||||
// Ignore splice calls guarded by an index equality check.
|
||||
// This indicates that the index of an element is the basis for removal, not its value,
|
||||
// which means it may be okay to skip over elements.
|
||||
not exists (ConditionGuardNode guard, EqualityTest test | cfg = guard |
|
||||
test = guard.getTest() and
|
||||
test.getAnOperand() = getIndexVariable().getAnAccess() and
|
||||
guard.getOutcome() = test.getPolarity()) and
|
||||
|
||||
// Block flow after inspecting an array element other than that at the current index.
|
||||
// For example, if the splice happens after inspecting `array[i + 1]`, then the next
|
||||
// element has already been "looked at" and so it doesn't matter if we skip it.
|
||||
not exists (IndexExpr index | cfg = index |
|
||||
array.flowsToExpr(index.getBase()) and
|
||||
not index.getIndex() = getIndexVariable().getAnAccess())
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if there is a `loop entry -> splice -> cfg` path that does not involve index manipulation,
|
||||
* other than the `splice` call.
|
||||
*/
|
||||
predicate hasPathThrough(SpliceCall splice, ControlFlowNode cfg) {
|
||||
splice = getACandidateSpliceCall() and
|
||||
cfg = splice.asExpr() and
|
||||
hasPathTo(cfg.getAPredecessor())
|
||||
or
|
||||
hasPathThrough(splice, cfg.getAPredecessor()) and
|
||||
getLoopEntry().dominates(cfg.getBasicBlock()) and
|
||||
not hasIndexingManipulation(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
from ArrayIterationLoop loop, SpliceCall splice
|
||||
where loop.hasPathThrough(splice, loop.getUpdate().getFirstControlFlowNode())
|
||||
select splice, "Removing an array item without adjusting the loop index '" + loop.getIndexVariable().getName() + "' causes the subsequent array item to be skipped."
|
||||
@@ -0,0 +1,9 @@
|
||||
function removePathTraversal(path) {
|
||||
let parts = path.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === '..') {
|
||||
parts.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return path.join('/');
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
function removePathTraversal(path) {
|
||||
let parts = path.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === '..') {
|
||||
parts.splice(i, 1);
|
||||
--i; // adjust for array shift
|
||||
}
|
||||
}
|
||||
return path.join('/');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
function removePathTraversal(path) {
|
||||
return path.split('/').filter(part => part !== '..').join('/');
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
| tst.js:4:27:4:44 | parts.splice(i, 1) | Removing an array item without adjusting the loop index 'i' causes the subsequent array item to be skipped. |
|
||||
| tst.js:13:29:13:46 | parts.splice(i, 1) | Removing an array item without adjusting the loop index 'i' causes the subsequent array item to be skipped. |
|
||||
| tst.js:24:9:24:26 | parts.splice(i, 1) | Removing an array item without adjusting the loop index 'i' causes the subsequent array item to be skipped. |
|
||||
@@ -0,0 +1 @@
|
||||
Statements/LoopIterationSkippedDueToShifting.ql
|
||||
@@ -0,0 +1,123 @@
|
||||
function removeX(string) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') parts.splice(i, 1); // NOT OK
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function removeXInnerLoop(string, n) {
|
||||
let parts = string.split('/');
|
||||
for (let j = 0; j < n; ++j) {
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') parts.splice(i, 1); // NOT OK
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function removeXOuterLoop(string, n) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
for (let j = 0; j < n; ++j) {
|
||||
if (parts[i] === 'X') {
|
||||
parts.splice(i, 1); // NOT OK
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function decrementAfter(string) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') {
|
||||
parts.splice(i, 1); // OK
|
||||
--i;
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function postDecrementArgument(string) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') {
|
||||
parts.splice(i--, 1); // OK
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
|
||||
function breakAfter(string) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') {
|
||||
parts.splice(i, 1); // OK - only removes first occurrence
|
||||
break;
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function insertNewElements(string) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') {
|
||||
parts.splice(i, 1, '.'); // OK - no shifting due to insert
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function spliceAfterLoop(string) {
|
||||
let parts = string.split('/');
|
||||
let i = 0;
|
||||
for (; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') break;
|
||||
}
|
||||
if (parts[i] === 'X') {
|
||||
parts.splice(i, 1); // OK - not inside loop
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function spliceAfterLoopNested(string) {
|
||||
let parts = string.split('/');
|
||||
for (let j = 0; j < parts.length; ++j) {
|
||||
let i = j;
|
||||
for (; i < parts.length; ++i) {
|
||||
if (parts[i] === 'X') break;
|
||||
}
|
||||
parts.splice(i, 1); // OK - not inside 'i' loop
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function removeAtSpecificPlace(string, k) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (i === k && parts[i] === 'X') parts.splice(i, 1); // OK - more complex logic
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function removeFirstAndLast(string) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (i === 0 || i === parts.length - 1) parts.splice(i, 1); // OK - out of scope of this query
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
|
||||
function inspectNextElement(string) {
|
||||
let parts = string.split('/');
|
||||
for (let i = 0; i < parts.length; ++i) {
|
||||
if (i < parts.length - 1 && parts[i] === parts[i + 1]) {
|
||||
parts.splice(i, 1); // OK - next element has been looked at
|
||||
}
|
||||
}
|
||||
return parts.join('/');
|
||||
}
|
||||
Reference in New Issue
Block a user