VNX-372: Incomplete Internal State Distinction
A substring or element search whose result has three states (not found, found at position 0, found later) is tested as a boolean, so two of the states are treated as one
| Field | Value |
|---|---|
| Rule ID | VNX-372 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | Python, PHP, JavaScript / TypeScript |
| CWE | CWE-372 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:L/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L |
| Tags | internal-state-distinction return-value cwe-372 |
| Source | internal/sast/rules/vnx-372.rego |
Overview
VNX-372 maps to CWE-372: Incomplete Internal State Distinction. A substring or element search whose result has three states (not found, found at position 0, found later) is tested as a boolean, so two of the states are treated as one. Reported in Python: str.find()/rfind() used directly as a condition (if/elif/while/assert, an and/or operand, a conditional expression, bool()) or compared with True/False, which treats ‘not found’ (-1) as found and a match at index 0 as missing. Reported in PHP: strpos/stripos/strrpos/strripos (also mb_ and iconv_ forms) used as a condition, negated with !, used in a ternary, or loosely compared (==, !=, <>) with false, true, null or 0, which conflates ‘not found’ (false) with a match at offset 0. Reported in JavaScript/TypeScript: indexOf/lastIndexOf/findIndex used directly as an if/while condition, a && or || operand, a ternary test, or loosely compared with true/false, which treats -1 as found and index 0 as missing. Not reported: comparisons with -1, >= 0, > -1, !== false or === 0; the starts-with idioms not s.find(x) and !s.indexOf(x) (true only at index 0) and ~s.indexOf(x); a result assigned and checked later; BeautifulSoup, ElementTree, lxml, Selenium and MongoDB find() (files that import them, receivers named soup/tree/root/node/element and keyword-argument calls); in, includes() and str_contains(); comments and string literals.
Why This Matters
A search such as str.find(), strpos() or indexOf() has three outcomes: not found, found at position 0, found later. Testing it as a boolean folds them into two, and the wrong two: -1 (not found) is truthy in Python and JavaScript, and a match at offset 0 is falsy in all three. In PHP false and 0 are both falsy, so a match at the start reads as no match. A check written this way lets through the value it was meant to catch.
What Gets Flagged
if content_type.find("json"): # FLAGGED: true when "json" is absent
return True
if (strpos($email, 'admin@') == false) { // FLAGGED: loose comparison
if (url.indexOf('/api/')) { // FLAGGED
return user.roles.indexOf('editor') && doc.editable; // FLAGGED
if (value.search(/@/)) { // FLAGGED: String.search() returns -1 too
if (array_search($id, $ids)) { // FLAGGED: false and key 0 are both falsy
What Is Not Flagged
if "json" in content_type: ...
if content_type.find("json") != -1: ...
if not path.find("/static"): ... # true only at index 0 (starts-with idiom)
if (strpos($email, 'admin@') !== false) { ... }
if (url.includes('/api/')) { ... }
if (!url.indexOf('/static/')) { ... } // starts-with idiom
BeautifulSoup, ElementTree, lxml, Selenium and MongoDB find() (which return an object or None), a find() in a Python file that defines its own def find( method, a JavaScript .search() whose argument is not a regex or string literal (a search client), comments and string literals are not reported.
Remediation
- Use the membership form:
x in s,str_contains(),includes(). - When you need the position, compare it explicitly with
-1(Python, JavaScript) or with!== false(PHP).
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-372
// nosec vnx-372
Or record an ignore rule: vulnetix ignore add --rule vnx-372 --reason "...". See Suppressing findings.