Conversation
…om user-controlled sources Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…tection (#183) * added advanced case history and prescriber/pharmacy change detection * run lint / prettier * ncpdp endpoint implimentation * update routing * add package ndc code tracking for ncpdp messages * fix ndc logic * updated models for patient id tracking * include response type in ncpdp messages * use saved medication on rems case creation to get ndc code from rxnorm * don't show patient status until patient enrolled * rxfill * proper reason code handling * singular denial reason * single denial reason
|
|
||
| logger.info(`Looking up case: ${caseId}`); | ||
|
|
||
| const remsCase = await remsCaseCollection.findOne({ case_number: caseId }); |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 10 hours ago
In general, to fix this kind of issue you must ensure that user-controlled values used in Mongo/NoSQL queries are interpreted only as literal values, not as query objects or operators. This can be done either by (1) validating/coercing the input so that only expected primitive types (string/number) are allowed, or (2) using an equality operator such as $eq so that even if an object is supplied, it is treated as a literal rather than merged into the query selector.
The best minimal-change fix here is to restrict caseId to a primitive string and reject requests that do not supply a valid string ID. That keeps the existing semantics of “lookup this case by its ID” while preventing query-shaping attacks. Concretely, within handleRemsRequest in src/ncpdp/script.ts, after extracting caseId, add a type check typeof caseId !== 'string' (and optionally a simple sanity check like .trim().length === 0) and return a “denied” response if the value is not valid. Then use caseId as before in findOne. This keeps the control flow and responses consistent with the existing “no case ID” / “case not found” branches and requires no new imports or external libraries.
More specifically:
- In
handleRemsRequest, afterconst caseId = ...and before logging “Extracted case ID”, insert a check ensuringcaseIdis a string (and not an object), returning a denied response (status 200, like the other validation error) if validation fails. - Optionally normalize
caseId(e.g.,const normalizedCaseId = caseId.trim();) and use the normalized version in the query; but to minimize behavior changes we will only check type and emptiness. - Leave the
findOne({ case_number: caseId })call intact so existing behavior is unchanged for valid string IDs.
No imports or additional methods are needed; all changes are within handleRemsRequest in src/ncpdp/script.ts.
| @@ -64,6 +64,14 @@ | ||
| .send(buildDeniedResponse(header, remsRequest, 'EC', 'Case ID not provided')); | ||
| } | ||
|
|
||
| if (typeof caseId !== 'string' || caseId.trim().length === 0) { | ||
| logger.error('Invalid Case ID type or value in request'); | ||
| res.type('application/xml'); | ||
| return res | ||
| .status(200) | ||
| .send(buildDeniedResponse(header, remsRequest, 'EC', 'Invalid case ID')); | ||
| } | ||
|
|
||
| logger.info(`Looking up case: ${caseId}`); | ||
|
|
||
| const remsCase = await remsCaseCollection.findOne({ case_number: caseId }); |
| const remsCase = await remsCaseCollection.findOne({ | ||
| patientFirstName: patient?.names?.name?.firstname, | ||
| patientLastName: patient?.names?.name?.lastname, | ||
| patientDOB: patient?.dateofbirth?.date, | ||
| drugNdcCode: drugNdcCode | ||
| }); |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 10 hours ago
In general, to fix NoSQL injection when building query objects from untrusted data, either (1) enforce that the values used in queries are primitives of the expected types (e.g., strings, dates) and not arbitrary objects, or (2) wrap them with an operator such as $eq so they are always interpreted as literals and not as part of the query structure. Input validation is usually preferable, because it also catches malformed requests earlier.
For this specific code, the safest, minimal-change approach is:
- Extract the relevant fields (
patientFirstName,patientLastName,patientDOB,drugNdcCode) into local variables. - Validate that each of these is of the expected primitive type (string) and not an object.
- If validation fails, respond with a 400 Bad Request and an appropriate error message (as XML, consistent with the rest of the handler) instead of querying the database.
- Pass only the validated primitive values into
remsCaseCollection.findOne. Because we now ensure they are strings (or at least non-object primitives), the query object cannot contain malicious operators.
All changes are confined to src/ncpdp/script.ts within the handleRemsInitiation function, around lines 193–215 and just before the catch at line 273. No new imports are required; we can reuse the existing buildErrorResponse, logger, and res handling patterns.
Concretely:
- Modify the section that builds
requestInfoand constructs thefindOnequery to first definepatientFirstName,patientLastName,patientDOB, anddrugNdcCodevariables. - Add a small validation block that checks
typeofeach field (and that it is present); on failure, logs and returns a 400 response withbuildErrorResponseand content typeapplication/xml. - Use the validated variables (not direct property access on
patient) in thefindOnefilter.
| @@ -200,16 +200,32 @@ | ||
| //const pharmacy = initRequest.pharmacy; | ||
| const drugNdcCode = initRequest.medicationprescribed?.product?.drugcoded?.ndc; | ||
|
|
||
| const patientFirstName = patient?.names?.name?.firstname; | ||
| const patientLastName = patient?.names?.name?.lastname; | ||
| const patientDOB = patient?.dateofbirth?.date; | ||
|
|
||
| const requestInfo = { | ||
| patientName: `${patient?.names?.name?.firstname} ${patient?.names?.name?.lastname}`, | ||
| patientName: `${patientFirstName} ${patientLastName}`, | ||
| drugNdcCode: drugNdcCode | ||
| }; | ||
| logger.info(`REMS Initiation request: ${JSON.stringify(requestInfo)}`); | ||
|
|
||
| // Validate that query fields are simple primitive values to avoid NoSQL injection | ||
| if ( | ||
| typeof patientFirstName !== 'string' || | ||
| typeof patientLastName !== 'string' || | ||
| typeof patientDOB !== 'string' || | ||
| typeof drugNdcCode !== 'string' | ||
| ) { | ||
| logger.error('Invalid REMS initiation request: patient/drug identifiers must be strings'); | ||
| res.type('application/xml'); | ||
| return res.status(400).send(buildErrorResponse('Invalid REMS initiation request')); | ||
| } | ||
|
|
||
| const remsCase = await remsCaseCollection.findOne({ | ||
| patientFirstName: patient?.names?.name?.firstname, | ||
| patientLastName: patient?.names?.name?.lastname, | ||
| patientDOB: patient?.dateofbirth?.date, | ||
| patientFirstName: patientFirstName, | ||
| patientLastName: patientLastName, | ||
| patientDOB: patientDOB, | ||
| drugNdcCode: drugNdcCode | ||
| }); | ||
|
|
src/ncpdp/script.ts
Outdated
| const medication = await medicationCollection.findOne({ | ||
| ndcCode: drugNdcCode | ||
| }); |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 10 hours ago
In general, untrusted values used in MongoDB queries should either be (a) wrapped in an operator like $eq so MongoDB always treats them as literal values, or (b) validated to ensure they are primitive types (e.g., string) and not objects containing query operators before being included in a query object.
For this code, the minimal, safe fix without changing functionality is to change the medicationCollection.findOne query (lines 227–229) to use the $eq operator for ndcCode. That keeps the same semantics (equality match on NDC code) but prevents a crafted object from being interpreted as a query operator. Concretely, we will modify the query in handleRemsInitiation from:
const medication = await medicationCollection.findOne({
ndcCode: drugNdcCode
});to:
const medication = await medicationCollection.findOne({
ndcCode: { $eq: drugNdcCode }
});This mirrors the "GOOD: using $eq operator" pattern in the background material. No additional imports or helper functions are required; the change is localized to the existing query in src/ncpdp/script.ts.
| @@ -225,7 +225,7 @@ | ||
|
|
||
| // Case exists - check requirements | ||
| const medication = await medicationCollection.findOne({ | ||
| ndcCode: drugNdcCode | ||
| ndcCode: { $eq: drugNdcCode } | ||
| }); | ||
|
|
||
| if (!medication) { |
| remsCase = await remsCaseCollection.findOne({ | ||
| patientFirstName: patientInfo.firstName, | ||
| patientLastName: patientInfo.lastName, | ||
| patientDOB: patientInfo.dob, | ||
| drugNdcCode: drugNdcCode | ||
| }); |
Check failure
Code scanning / CodeQL
Database query built from user-controlled sources High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI about 10 hours ago
In general, to fix this type of issue in MongoDB-style queries, you must ensure that any user-controlled values embedded in a query filter are interpreted as literal values, not as query objects. This can be done by either (a) wrapping them with $eq so MongoDB treats them as equality comparisons on a literal, or (b) validating/coercing the values to the expected primitive types (e.g., strings, dates) and rejecting or sanitizing objects.
For this specific case, the simplest change that preserves existing functionality is to make each user-derived field in the findOne filter explicitly use $eq. That way, even if an attacker sends an object like { $ne: null } as drugNdcCode, it will be used as the literal value of the equality comparison instead of being interpreted as an operator. We can keep the rest of the logic intact: still look up by first name, last name, DOB, and NDC when drugNdcCode is present. Concretely, in src/ncpdp/script.ts, in the handleRxFill logic around line 308–314, we should change the findOne call to:
remsCase = await remsCaseCollection.findOne({
patientFirstName: { $eq: patientInfo.firstName },
patientLastName: { $eq: patientInfo.lastName },
patientDOB: { $eq: patientInfo.dob },
drugNdcCode: { $eq: drugNdcCode }
});This does not require new imports or helper functions and doesn’t alter the logical behavior for legitimate scalar inputs; it simply hardens the query against operator injection.
| @@ -307,10 +307,10 @@ | ||
|
|
||
| if (drugNdcCode) { | ||
| remsCase = await remsCaseCollection.findOne({ | ||
| patientFirstName: patientInfo.firstName, | ||
| patientLastName: patientInfo.lastName, | ||
| patientDOB: patientInfo.dob, | ||
| drugNdcCode: drugNdcCode | ||
| patientFirstName: { $eq: patientInfo.firstName }, | ||
| patientLastName: { $eq: patientInfo.lastName }, | ||
| patientDOB: { $eq: patientInfo.dob }, | ||
| drugNdcCode: { $eq: drugNdcCode } | ||
| }); | ||
| } | ||
|
|
Describe your changes
Check the ETASU status at the dispense authorization endpoint, basic fill in for NCPDP messages using JSON for now (will change to NCPDP XML once we receive spec) and send a communication resource to the FHIR EHR server for the prescriber to see.
Issue ticket number and Jira link
REMS-856
Checklist before requesting a review
devnot main (the only exception to this is releases fromdevand hotfix branches)Checklist for conducting a review
Workflow
Owner of the Pull Request will be responsible for merge after all requirements are met, including approval from at least one reviewer. Additional changes made after a review will dismiss any approvals and require re-review of the additional updates. Auto merging can be enabled below if additional changes are likely not to be needed. The bot will auto assign reviewers to your Pull Request for you.