Merge pull request #954 from bbassie/feature/redact-mod-portal-token

Redact credentials from URLs in download errors
This commit is contained in:
Cooldude2606 authored and GitHub committed 2026-07-29 23:02:12 +01:00
commit e4c2ccd58f
4 files changed
+104 -1

No files matched your search

+17
View File
@@ -291,6 +291,23 @@ describe("lib/file_ops", function() {
);
});
it("should not leak credentials from the url when throwing", async function () {
// The mod portal takes the Factorio token as a query parameter, see #795.
const downloadPath = path.join(downloadDir, "credentials.txt");
const url = new URL("/not-found", baseUrl);
url.searchParams.set("username", "user");
url.searchParams.set("token", "secret");
await assert.rejects(
lib.downloadFile(url, downloadPath, "overwrite"),
err => {
assert.ok(!err.message.includes("secret"), `token leaked in: ${err.message}`);
assert.match(err.message, /token=REDACTED/);
assert.match(err.message, /username=user/);
return true;
}
);
});
it("should throw if fetch response body is missing", async function () {
const downloadPath = path.join(downloadDir, "not-content.txt");
await assert.rejects(
+39
View File
@@ -236,6 +236,45 @@ describe("lib/helpers", function() {
});
});
describe("redactUrl()", function() {
it("should redact the value of credential query parameters", function() {
assert.equal(
lib.redactUrl("https://mods.factorio.com/download/mod?username=user&token=secret"),
"https://mods.factorio.com/download/mod?username=user&token=REDACTED"
);
for (const name of ["access_token", "api_key", "apikey", "password", "secret", "token"]) {
assert.equal(
lib.redactUrl(`https://example.com/?${name}=secret`),
`https://example.com/?${name}=REDACTED`
);
}
});
it("should match credential parameter names case insensitively", function() {
assert.equal(lib.redactUrl("https://example.com/?Token=secret"), "https://example.com/?Token=REDACTED");
assert.equal(lib.redactUrl("https://example.com/?API_KEY=secret"), "https://example.com/?API_KEY=REDACTED");
});
it("should redact every occurrence of a repeated credential parameter", function() {
assert.equal(
lib.redactUrl("https://example.com/?token=first&token=second"),
"https://example.com/?token=REDACTED&token=REDACTED"
);
});
it("should accept a URL object", function() {
assert.equal(
lib.redactUrl(new URL("https://example.com/path?token=secret")),
"https://example.com/path?token=REDACTED"
);
});
it("should leave URLs without credentials untouched", function() {
assert.equal(lib.redactUrl("https://example.com/path"), "https://example.com/path");
assert.equal(lib.redactUrl("https://example.com/?page=2"), "https://example.com/?page=2");
});
it("should return unparsable input unchanged", function() {
assert.equal(lib.redactUrl("not a url?token=secret"), "not a url?token=secret");
assert.equal(lib.redactUrl(""), "");
});
});
function parse(input, attributes) {
const result = lib.parseSearchString(input, attributes);
if (!result.issues.length) {