VNX-501: Trust Boundary Violation
Detects HTTP request data (parameters, headers, cookies, query string, Spring request arguments, Flask/Django request fields) stored in the server-side session without validation, inline, through a few assignments or a list or map; operands the enclosing method proves constant are not reported. Java and Python.
| Field | Value |
|---|---|
| Rule ID | VNX-501 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Java, Python |
| CWE | CWE-501 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | trust-boundary session cwe-501 |
| Source | internal/sast/rules/vnx-501.rego |
Overview
VNX-501 maps to CWE-501: Trust Boundary Violation. The server-side session is trusted state: code that reads session.getAttribute("role") or session["user_id"] assumes the server put it there. Storing a value straight from the request in the session moves untrusted data across that boundary, so a later check that trusts the session trusts the attacker.
The rule reports:
- Java:
HttpSession.setAttribute()/putValue()(on a session variable or onrequest.getSession()) whose key or value isgetParameter*,getHeader*,getCookies,getQueryString,getReader,getInputStream,getPathInfo,getRequestURI, or a Spring@RequestParam/@PathVariable/@RequestHeader/@CookieValue/@RequestBodyargument. The value may arrive inline or through any chain of assignments up to twelve deep in the 60 lines above, including a for-each overgetCookies(), a single-lineif (...) x = y;, an assignment whose right side continues on the following lines, an object built from the request (new Reader(request)) whose getters are then called, a map entry read back under the same literal key, and a helper method of the same file that returns its parameter, unless every argument it was handed is a constant. - Python:
session[...] = ...(Flasksession,flask.session, Djangorequest.session) andsession.update()/setdefault()with a key or value fromrequest.args/form/values/cookies/headers/json/data/query_string/get_data/stream/files/GET/POST/COOKIES/META, or from an object built fromrequest, through the same kind of assignments (f-string interpolation included).
What Gets Flagged
// FLAGGED: request parameter becomes session state
String role = req.getParameter("role");
req.getSession().setAttribute("role", role);
// FLAGGED: Spring request argument stored in the session
public String applyDiscount(@RequestParam("code") String code, HttpSession session) {
session.setAttribute("discountCode", code);
# FLAGGED
session["next_url"] = request.args.get("next")
request.session["lang"] = request.GET["lang"]
What Is Not Flagged
- Constants and server-generated values (
session.setAttribute("nonce", UUID.randomUUID().toString())). - A value checked first by a line that names it with
.contains(),.matches(),re.match, avalid*/sanitize*call orin ALLOWED. - A session written after a password check or
authenticate()call: the identity the server established. - A map read under a different key than the one the request value was stored under, and a same-file helper whose return value is not derived from its parameters.
- A name that the enclosing method or function (at most 120 lines up) proves holds a constant where the session is written: a literal it was last assigned on every path; the branch an
if/elseor conditional expression takes when its condition folds to a constant (integer literals and integer locals assigned once, or in Python a literal tested within/not inagainst a string local); the case aswitchormatchon a constant selects; the element a list index or a map, dict orConfigParserkey actually holds when that is a literal; in Python, a copy (saved = label) of what the name held at that line; the result of a same-file method or function whose every return is constant; in Python, a getter of a request-wrapper object whose method, declared anywhere in the project, returns only literals (a “safe value” accessor); a literal tested inside a string local ('x' in label) is a substring test on a constant and is not an allowlist check onlabel. - Reading the session, comments and string literals.
coupon = request.form.get("coupon", "")
fields = {"currency": "EUR"}
fields["coupon"] = coupon
currency = fields.get("currency")
session["cart.currency"] = currency # not flagged: that key holds "EUR"
code = fields.get("coupon")
session["cart.coupon"] = code # flagged
A value stored in a list or a Python dict is followed to a later read of that collection. Anything the analysis cannot resolve stays reported: a condition on run-time data, a helper in another file, a collection passed to a call.
Calls that cannot bring data in
A call into code the scan cannot see (another class or module) is treated as inert, and its value as a constant, when all of these hold:
- its name is not that of a source (
read*,next*,getParameter*,getHeader*,getCookie*,getenv,getProperty,query,execute); - every argument is a constant: a literal, or a name the method fixes;
- its receiver is a type name, a
newexpression with constant arguments, or a local built from such a call, and that local is only ever called with constant arguments.
// NOT REPORTED: the helper is given literals only
ModeBook book = ModeBooks.standard();
String channel = book.entry("web");
session.setAttribute("shop.channel", channel);
// REPORTED: a run-time argument
String channel = book.entry(coupon);
// REPORTED: the receiver was built from, or fed, a run-time value
ModeBook scoped = new ModeBook(request.getParameter("scope"));
String region = scoped.entry("eu");
// REPORTED: a source-like method name
String channel = Prompts.readLine("channel");
Anything that fails one of the tests stays reported, so a helper that hands back its argument is never mistaken for a constant.
Remediation
- Validate the value against an allowlist or a strict pattern before it enters the session.
- Prefer storing a server-side lookup result (the user record found by id) over the raw request text.
- Keep authorisation state (roles, ownership) derived on the server, never copied from the request.
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-501
// nosec vnx-501
Or record an ignore rule: vulnetix ignore add --rule vnx-501 --reason "...". See Suppressing findings.