Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions api/src/controllers/script-runner.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,23 @@ export class ScirptRunnerController {
);
}

@Put('transferListingBuildingSelectionCriteriaAssetsData')
@ApiOperation({
summary:
'A script that pulls listing asset data from one source into the current db',
operationId: 'transferListingBuildingSelectionCriteriaAssetsData',
})
@ApiOkResponse({ type: SuccessDTO })
async transferListingBuildingSelectionCriteriaAssetsData(
@Body() dataTransferDTO: AssetTransferDTO,
@Request() req: ExpressRequest,
): Promise<SuccessDTO> {
return await this.scriptRunnerService.transferListingBuildingSelectionCriteriaAssetsData(
req,
dataTransferDTO,
);
}

@Put('transferJurisdictionPartnerUserData')
@ApiOperation({
summary:
Expand Down
132 changes: 132 additions & 0 deletions api/src/services/script-runner.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,138 @@ export class ScriptRunnerService {
return { success: true };
}

/**
*
* @param req incoming request object
* @param dataTransferDTO data transfer endpoint args. Should contain foreign db connection string
* @returns successDTO
* @description transfers building selection criteria assets for listings in the specified space into the new space
*/
async transferListingBuildingSelectionCriteriaAssetsData(
req: ExpressRequest,
dataTransferDTO: AssetTransferDTO,
prisma?: PrismaClient,
): Promise<SuccessDTO> {
// script runner standard start up
const requestingUser = mapTo(User, req['user']);
await this.markScriptAsRunStart(
`data transfer building selection criteria ${dataTransferDTO.jurisdiction}`,
requestingUser,
);

// connect to foreign db based on incoming connection string
const client =
prisma ||
new PrismaClient({
datasources: {
db: {
url: dataTransferDTO.connectionString,
},
},
});
await client.$connect();

const doorwayJurisdiction = await this.prisma.jurisdictions.findFirst({
where: { name: dataTransferDTO.jurisdiction },
});

if (!doorwayJurisdiction) {
throw new Error(
`${dataTransferDTO.jurisdiction} county doesn't exist in Doorway database`,
);
}

// get jurisdiction
const jurisdiction: { id: string }[] =
await client.$queryRaw`SELECT id, name FROM jurisdictions WHERE name = ${dataTransferDTO.jurisdiction}`;

if (!jurisdiction) {
throw new Error(
`${dataTransferDTO.jurisdiction} county doesn't exist in foreign database`,
);
}
const listingTransferMap = await this.prisma.listingTransferMap.findMany({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused on what the purpose of this listingTransferMap layer. The listing_id is always the same as the old_id and doesn't take jurisdiction into account. So when we go to use this for San Jose it will also try to do the listings of the Alameda listings we transferred.

orderBy: {
listingId: OrderByEnum.ASC,
},
});
console.log(`Found ${listingTransferMap.length} listings`);
// loop over each new listing id <-> old listing id relation
for (let i = 0; i < listingTransferMap.length; i++) {
const oldAssetInfo: {
created_at: Date;
updated_at: Date;
file_id: string;
label: string;
}[] = await client.$queryRaw`SELECT
a.created_at,
a.updated_at,
a.file_id,
a.label
FROM listings l
JOIN assets a ON a.id = l.building_selection_criteria_file_id
WHERE l.id = ${listingTransferMap[i].oldId} :: UUID
AND l.building_selection_criteria_file_id IS NOT NULL`;
console.log(
`moving ${oldAssetInfo.length || 0} assets for listing: ${
listingTransferMap[i].oldId
}:`,
);
// loop over each listing image on the old listing
for (let j = 0; j < oldAssetInfo.length; j++) {
// pull down image from cloudinary
const image = await axios.get(
`https://res.cloudinary.com/${dataTransferDTO.cloudinaryName}/image/upload/${oldAssetInfo[j].file_id}.pdf`,
{
responseType: 'arraybuffer',
},
);
Comment on lines +1176 to +1181

Check failure

Code scanning / CodeQL

Server-side request forgery

The [URL](1) of this request depends on a [user-provided value](2).

Copilot Autofix

AI over 1 year ago

To fix the SSRF vulnerability, we need to validate and restrict the cloudinaryName value to a predefined set of allowed values (allow-list). This ensures that only trusted subdomains are used in the URL construction. The best way to implement this is to:

  1. Define an allow-list of valid cloudinaryName values.
  2. Validate the cloudinaryName against this allow-list before using it in the URL.
  3. Reject or handle invalid values appropriately.

The validation should be implemented in the transferListingBuildingSelectionCriteriaAssetsData method in api/src/services/script-runner.service.ts. If the cloudinaryName is invalid, an exception should be thrown to prevent further processing.


Suggested changeset 1
api/src/services/script-runner.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/services/script-runner.service.ts b/api/src/services/script-runner.service.ts
--- a/api/src/services/script-runner.service.ts
+++ b/api/src/services/script-runner.service.ts
@@ -1175,2 +1175,6 @@
         // pull down image from cloudinary
+        const allowedCloudinaryNames = ['trusted-cloudinary-name1', 'trusted-cloudinary-name2'];
+        if (!allowedCloudinaryNames.includes(dataTransferDTO.cloudinaryName)) {
+          throw new BadRequestException('Invalid cloudinaryName provided.');
+        }
         const image = await axios.get(
EOF
@@ -1175,2 +1175,6 @@
// pull down image from cloudinary
const allowedCloudinaryNames = ['trusted-cloudinary-name1', 'trusted-cloudinary-name2'];
if (!allowedCloudinaryNames.includes(dataTransferDTO.cloudinaryName)) {
throw new BadRequestException('Invalid cloudinaryName provided.');
}
const image = await axios.get(
Copilot is powered by AI and may make mistakes. Always verify output.
const newFileId = (oldAssetInfo[j].file_id as string)
.replace('housingbayarea/', '')
.replace('dev/', '');

// upload image to s3
const res = await this.assetService.upload(newFileId, {
filename: null,
buffer: image.data,
fieldname: null,
originalname: `${newFileId}.pdf`,
encoding: null,
mimetype: 'application/pdf',
size: image.data.length,
destination: null,
path: null,
stream: null,
});

// update new listing with these assets
await this.prisma.listings.update({
where: {
id: listingTransferMap[i].listingId,
},
data: {
listingsBuildingSelectionCriteriaFile: {
create: {
fileId: res.url,
label: 'cloudinaryPDF',
},
},
},
});
}
}

// disconnect from foreign db
await client.$disconnect();

// script runner standard spin down
await this.markScriptAsComplete(
`data transfer building selection criteria ${dataTransferDTO.jurisdiction}`,
requestingUser,
);
return { success: true };
}

/**
*
* @param amiChartImportDTO this is a string in a very specific format like:
Expand Down
28 changes: 22 additions & 6 deletions shared-helpers/src/types/backend-swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2322,6 +2322,28 @@ export class ScriptRunnerService {
axios(configs, resolve, reject)
})
}
/**
* A script that pulls listing asset data from one source into the current db
*/
transferListingBuildingSelectionCriteriaAssetsData(
params: {
/** requestBody */
body?: AssetTransferDTO
} = {} as any,
options: IRequestOptions = {}
): Promise<SuccessDTO> {
return new Promise((resolve, reject) => {
let url = basePath + "/scriptRunner/transferListingBuildingSelectionCriteriaAssetsData"

const configs: IRequestConfig = getConfigs("put", "application/json", url, options)

let data = params.body

configs.data = data

axios(configs, resolve, reject)
})
}
/**
* A script that pulls partner user data from one source into the current db
*/
Expand Down Expand Up @@ -6962,12 +6984,6 @@ export enum EnumUnitGroupAmiLevelMonthlyRentDeterminationType {
"flatRent" = "flatRent",
"percentageOfIncome" = "percentageOfIncome",
}
export enum HomeTypeEnum {
"apartment" = "apartment",
"duplex" = "duplex",
"house" = "house",
"townhome" = "townhome",
}
export enum EnumUnitGroupAmiLevelCreateMonthlyRentDeterminationType {
"flatRent" = "flatRent",
"percentageOfIncome" = "percentageOfIncome",
Expand Down