fix: harden anonymization privacy paths (#754)

This commit is contained in:
Thomas Durieux
2026-07-22 04:28:01 +02:00
committed by GitHub
parent 5ef10dee9e
commit aab6ccf7fc
5 changed files with 262 additions and 50 deletions
+37
View File
@@ -2,6 +2,10 @@ const { expect } = require("chai");
const { Transform } = require("stream");
const { StringDecoder } = require("string_decoder");
require("ts-node/register/transpile-only");
const {
ContentAnonimizer: RealContentAnonimizer,
AnonymizeTransformer: RealAnonymizeTransformer,
} = require("../src/core/anonymize-utils");
const {
withWordBoundaries,
termVariants,
@@ -449,6 +453,20 @@ describe("ContentAnonimizer", function () {
expect(result).to.include("anonymous.4open.science/r/abc123");
});
it("escapes regex characters in repository and branch names", function () {
const anon = new RealContentAnonimizer({
repoName: "secret-owner/secret.repo",
branchName: "release+candidate",
repoId: "abc123",
});
const result = anon.anonymize(
"https://raw.githubusercontent.com/secret-owner/secret.repo/release+candidate/src/index.ts"
);
expect(result).to.equal(
"https://anonymous.4open.science/r/abc123/src/index.ts"
);
});
it("is case-insensitive for repo name", function () {
const anon = new ContentAnonimizer({
repoName: "Owner/Repo",
@@ -647,6 +665,25 @@ describe("AnonymizeTransformer (streaming)", function () {
expect(result).to.equal("Created by XXXX-1 at 2025/01/01");
});
it("does not leak matches longer than the normal streaming overlap", async function () {
const secret = "A".repeat(5000);
const transformer = new RealAnonymizeTransformer({
filePath: "fixture.txt",
terms: ["A{5000}"],
});
const chunks = [];
const done = new Promise((resolve, reject) => {
transformer.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
transformer.on("end", () =>
resolve(Buffer.concat(chunks).toString("utf8"))
);
transformer.on("error", reject);
});
transformer.end(Buffer.from(` ${secret} `));
const result = await done;
expect(result).to.equal(" XXXX-1 ");
});
// Regression for #342/#349: zip download was constructing the transformer
// and then assigning opt.filePath after the fact, but isText is decided in
// the constructor — so every entry was treated as binary and passed through
+46
View File
@@ -0,0 +1,46 @@
const { expect } = require("chai");
require("ts-node/register/transpile-only");
const AnonymizedFile = require("../src/core/AnonymizedFile").default;
const FileModel = require("../src/core/model/files/files.model").default;
describe("AnonymizedFile custom replacement paths", function () {
let originalFindOne;
let originalFind;
beforeEach(function () {
originalFindOne = FileModel.findOne;
originalFind = FileModel.find;
});
afterEach(function () {
FileModel.findOne = originalFindOne;
FileModel.find = originalFind;
});
it("maps a custom replacement back to the original file", async function () {
const original = {
repoId: "repo-1",
path: "src/secret",
name: "index.ts",
sha: "abc",
size: 10,
};
FileModel.findOne = async () => null;
FileModel.find = () => ({ exec: async () => [original] });
const repository = {
repoId: "repo-1",
options: { terms: ["secret=>hidden"] },
model: { truncatedFolders: [] },
source: {},
};
const file = new AnonymizedFile({
repository,
anonymizedPath: "src/hidden/index.ts",
});
expect(await file.getFileInfo()).to.equal(original);
expect(await file.originalPath()).to.equal("src/secret/index.ts");
});
});