A request body is data, never a query - #812
Conversation
MongoDB has no query language separate from its data: a filter is an
ordinary object and a `$` key inside one is an operator. So a value that
arrives from a caller and lands in a filter can stop being a value.
{ user: 'amudha' } the one row
{ user: { $ne: null } } every row
Measured rather than assumed, on the real stack. Express 5's default
query parser is the simple one, so `?user[$ne]=x` arrives as the literal
string key "user[$ne]" and is inert - the query string was already safe.
A JSON body is not: `{"user":{"$ne":null}}` reaches a handler intact, and
`express.urlencoded({ extended: true })` means a form post is as capable
of it.
Nothing in this product needs a caller to name an operator - the server
writes its own $set, its own $gte on a date range, its own $or - so one
is removed immediately after the parsers rather than guarded for at six
hundred call sites.
It STRIPS rather than refuses. A 400 would be the better answer for a new
API; this one has several clients including tills a shop cannot update
today, and one caller sending a stray $ key in a field nobody reads would
start failing sales. A shop losing orders is worse than a payload being
made inert. The removal is logged with the count and the field path, never
the value, because a log line built from caller input is its own problem.
Dots are left alone: `invoice.prefix` is ordinary data here and a dot
cannot introduce an operator. __proto__, constructor and prototype go with
the operators, since a key that reaches an assignment is its own problem.
This is what CodeQL's js/sql-injection findings are about, and there are
251 of them. Every one I read is a false positive - they are ObjectId
casts and allow-list checks CodeQL does not model as sanitisers - so this
is defence in depth at the edge rather than 251 individual changes.
|
Measured the two premises against the real stack before this lands, because #811 merged into the same region twenty minutes ago and the two descriptions disagree. The body is already stripped. So The query string was the half that was broken, which is the opposite of what the description says. You are right that The genuinely new finding here is not in the diff. Not closing this — that is the owner's call. But as it stands it duplicates a working guard and leaves two |
|
Merged to Try it at https://develop.posnic.io, or run it yourself: git fetch origin develop && git checkout develop
npm install && npm --prefix api install
npm run dev # then http://localhost:3000When you have tested it, say what you did and what happened, and set Reporting that something is broken is as useful as fixing it. It is |
You asked me to work through the code-scanning list. The honest headline first: it is 626 open alerts, not 27 - I read only the first page last time and reported its length. Sorry.
What is actually in there
js/sql-injectionjs/xss-through-domjs/missing-rate-limitingjs/log-injectionThe
js/sql-injectionones are CodeQL not modelling this codebase's sanitisers. Every site I opened casts or allow-lists before the value reaches a filter:Changing 251 call sites would be work with no security value.
So this fixes the class at the edge instead
Measured on the real stack rather than assumed:
?user[$ne]=xarrives as the literal string key"user[$ne]"and is inert.{"user":{"$ne":null}}reaches a handler intact, andexpress.urlencoded({ extended: true })makes a form post just as capable.Nothing here needs a caller to name an operator - the server writes its own
$set, its own$gteon a date range, its own$or- so one is removed straight after the parsers.I did find one place where it would actually have worked:
activityLogger.getActivityLogsdoesquery.user = userIdwithuserIdstraight off the request and no cast, then hands it to Mongoose.branchandlicensestill bound it to the shop, so it is a within-shop authorization weakening rather than a tenant break, but it is real, and it is the shape all 251 alerts are pointing at.It strips rather than refuses
A 400 is louder and would be right for a new API. This one has several clients including tills a shop cannot update today, and one caller sending a stray
$key in a field nobody reads would start failing sales. A shop losing orders is worse than a payload being made inert.Logged with the count and the field path, never the value - a log line built from caller input is its own alert class.
Dots are left alone:
invoice.prefixis ordinary data in settings here, and a dot cannot introduce an operator.__proto__,constructorandprototypego with the operators.Checks
req, because half of what is asserted is where it sits in the stackapp.set('query parser', 'extended')fails loudly instead of quietly opening the door this shutsA$AP,paid $20andprice$all go through untouchedfrontend/publicbuilt, which CI does);check-locally.jsall 7 passStill open
The other 375 alerts.
js/xss-through-dom(55) is the one I would look at next, since it is the class that can actually reach a till operator's screen.