Skip to content
Merged
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
24 changes: 6 additions & 18 deletions src/common/exportsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
public async readExport(exportName: string): Promise<string> {
try {
this.assertIsNotShuttingDown();
exportName = decodeURIComponent(exportName);
exportName = decodeAndNormalize(exportName);
const exportHandle = this.storedExports[exportName];
if (!exportHandle) {
throw new Error("Requested export has either expired or does not exist.");
Expand Down Expand Up @@ -163,7 +163,7 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
}): Promise<AvailableExport> {
try {
this.assertIsNotShuttingDown();
const exportNameWithExtension = validateExportName(ensureExtension(exportName, "json"));
const exportNameWithExtension = decodeAndNormalize(ensureExtension(exportName, "json"));
if (this.storedExports[exportNameWithExtension]) {
return Promise.reject(
new Error("Export with same name is either already available or being generated.")
Expand Down Expand Up @@ -363,6 +363,10 @@ export class ExportsManager extends EventEmitter<ExportsManagerEvents> {
}
}

export function decodeAndNormalize(text: string): string {
return decodeURIComponent(text).normalize("NFKC");
}

/**
* Ensures the path ends with the provided extension */
export function ensureExtension(pathOrName: string, extension: string): string {
Expand All @@ -373,22 +377,6 @@ export function ensureExtension(pathOrName: string, extension: string): string {
return `${pathOrName}${extWithDot}`;
}

/**
* Small utility to decoding and validating provided export name for path
* traversal or no extension */
export function validateExportName(nameWithExtension: string): string {
const decodedName = decodeURIComponent(nameWithExtension);
if (!path.extname(decodedName)) {
throw new Error("Provided export name has no extension");
}

if (decodedName.includes("..") || decodedName.includes("/") || decodedName.includes("\\")) {
throw new Error("Invalid export name: path traversal hinted");
}

return decodedName;
}

export function isExportExpired(createdAt: number, exportTimeoutMs: number): boolean {
return Date.now() - createdAt > exportTimeoutMs;
}
7 changes: 6 additions & 1 deletion src/resources/common/exportedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,12 @@ export class ExportedData {
private autoCompleteExportName: CompleteResourceTemplateCallback = (value) => {
try {
return this.session.exportsManager.availableExports
.filter(({ exportName }) => exportName.startsWith(value))
.filter(({ exportName, exportTitle }) => {
const lcExportName = exportName.toLowerCase();
const lcExportTitle = exportTitle.toLowerCase();
const lcValue = value.toLowerCase();
return lcExportName.startsWith(lcValue) || lcExportTitle.includes(lcValue);
})
.map(({ exportName }) => exportName);
} catch (error) {
this.session.logger.error({
Expand Down
2 changes: 1 addition & 1 deletion src/tools/mongodb/read/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export class ExportTool extends MongoDBToolBase {
});
}

const exportName = `${database}.${collection}.${new ObjectId().toString()}.json`;
const exportName = `${new ObjectId().toString()}.json`;

const { exportURI, exportPath } = await this.session.exportsManager.createJSONExport({
input: cursor,
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/resources/exportedData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,10 @@ describeWithMongoDB(
},
argument: {
name: "exportName",
value: "b",
value: "big",
},
});
expect(completeResponse.completion.total).toEqual(1);
expect(completeResponse.completion.total).toBeGreaterThanOrEqual(1);
});
});
},
Expand Down
25 changes: 3 additions & 22 deletions tests/unit/common/exportsManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import type { FindCursor } from "mongodb";
import { Long } from "mongodb";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ExportsManagerConfig } from "../../../src/common/exportsManager.js";
import {
ensureExtension,
isExportExpired,
ExportsManager,
validateExportName,
} from "../../../src/common/exportsManager.js";
import { ensureExtension, isExportExpired, ExportsManager } from "../../../src/common/exportsManager.js";
import type { AvailableExport } from "../../../src/common/exportsManager.js";
import { config } from "../../../src/common/config.js";
import { ROOT_DIR } from "../../accuracy/sdk/constants.js";
Expand All @@ -30,14 +25,10 @@ const exportsManagerConfig: ExportsManagerConfig = {
function getExportNameAndPath({
uniqueExportsId = new ObjectId().toString(),
uniqueFileId = new ObjectId().toString(),
database = "foo",
collection = "bar",
}:
| {
uniqueExportsId?: string;
uniqueFileId?: string;
database?: string;
collection?: string;
}
| undefined = {}): {
sessionExportsPath: string;
Expand All @@ -46,7 +37,7 @@ function getExportNameAndPath({
exportURI: string;
uniqueExportsId: string;
} {
const exportName = `${database}.${collection}.${uniqueFileId}.json`;
const exportName = `${uniqueFileId}.json`;
// This is the exports directory for a session.
const sessionExportsPath = path.join(exportsPath, uniqueExportsId);
const exportPath = path.join(sessionExportsPath, exportName);
Expand Down Expand Up @@ -248,7 +239,7 @@ describe("ExportsManager unit test", () => {
});

it("should handle encoded name", async () => {
const { exportName, exportURI } = getExportNameAndPath({ database: "some database", collection: "coll" });
const { exportName, exportURI } = getExportNameAndPath({ uniqueFileId: "1FOO 2BAR" });
const { cursor } = createDummyFindCursor([]);
const exportAvailableNotifier = getExportAvailableNotifier(encodeURI(exportURI), manager);
await manager.createJSONExport({
Expand Down Expand Up @@ -611,16 +602,6 @@ describe("#ensureExtension", () => {
});
});

describe("#validateExportName", () => {
it("should return decoded name when name is valid", () => {
expect(validateExportName(encodeURIComponent("Test Name.json"))).toEqual("Test Name.json");
});
it("should throw when name is invalid", () => {
expect(() => validateExportName("NoExtension")).toThrow("Provided export name has no extension");
expect(() => validateExportName("../something.json")).toThrow("Invalid export name: path traversal hinted");
});
});

describe("#isExportExpired", () => {
it("should return true if export is expired", () => {
const createdAt = Date.now() - 1000;
Expand Down
Loading