diff --git a/src/models/callStack.js b/src/models/callStack.js index 66545e20..3aec037d 100644 --- a/src/models/callStack.js +++ b/src/models/callStack.js @@ -20,23 +20,64 @@ module.exports = class CallStack { // Attempts to remove any sensitive data that may be found within sanitize(content) { - if (typeof content !== "object") { - return content; - } + const badKeys = [ + "token", + "password", + "pass", + "auth", + "secret", + "passphrase", + "card" + ]; + const githubTokenReg = /(?:gho_|ghp_|github_pat_|ghu_|ghs_|ghr_)/; + const hideString = "*****"; let outContent = {}; + let type = typeof content; - for (const key in content) { - switch (key) { - case "token": - outContent[key] = "*****"; - break; - default: - outContent[key] = content[key]; - break; + // Since JavaScript `typeof` will assign an array as "object" as well as null + // we will extend this typeof check to add those as different types, to ease + // the complexity of the below switch statement + if (type === "object") { + if (Array.isArray(content)) { + type = "array"; + } else if (content === null) { + type = "null"; } } + switch(type) { + case "object": + for (const key in content) { + // Match different possible keys that represent sensitive data + if (badKeys.includes(key)) { + outContent[key] = hideString; + } else { + outContent[key] = this.sanitize(content[key]); + } + } + break; + case "string": + // Match different strings of sensitive data + + // https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github#githubs-token-formats + if (githubTokenReg.test(content)) { + outContent = hideString; + } else { // More strings to test can be added here + // String seems safe + outContent = content; + } + break; + case "array": + outContent = []; + for (let i = 0; i < content.length; i++) { + outContent.push(this.sanitize(content[i])); + } + break; + default: + outContent = content; + } + return outContent; } }; diff --git a/tests/unit/models/callStack.test.js b/tests/unit/models/callStack.test.js new file mode 100644 index 00000000..54363965 --- /dev/null +++ b/tests/unit/models/callStack.test.js @@ -0,0 +1,82 @@ +const callStack = require("../../../src/models/callStack.js"); +const hideValue = "*****"; + +describe("Sanitizes content as expected", () => { + test("Leaves safe data unmodified", () => { + const cs = new callStack(); + + const before = { + value: "Safe data" + }; + + const after = cs.sanitize(before); + + expect(after).toEqual(before); + }); + + test("Removes value of a key starting with 'token'", () => { + const cs = new callStack(); + + const before = { + token: "super_secret" + }; + + const after = cs.sanitize(before); + + expect(after).toEqual({ token: hideValue }); + }); + + test("Removes value of an unsafe string", () => { + const cs = new callStack(); + + const before = "gho_value"; + + const after = cs.sanitize(before); + + expect(after).toEqual(hideValue); + }); + + test("Removes deeply nested unsafe string", () => { + const cs = new callStack(); + + const before = { value: { value: { value: { value: "github_pat_value" }}}}; + + const after = cs.sanitize(before); + + expect(after).toEqual({ value: { value: { value: { value: hideValue }}}}); + }); + + test("Removes unsafe value from array", () => { + const cs = new callStack(); + + const before = [{}, {}, { token: "super_secret" }]; + const after = cs.sanitize(before); + + expect(after).toEqual([{}, {}, { token: hideValue }]); + }); + + test("Doesn't break on 'null'", () => { + const cs = new callStack(); + + const before = null; + const after = cs.sanitize(before); + + expect(after).toEqual(null); + }); + + test("Removes all known github token formats", () => { + const cs = new callStack(); + + const before = [ + { v1: "ghp_personal_access_token_classic" }, + { v2: "github_pat_fine_grained_personal_access_token" }, + { v3: "gho_oauth_access_token" }, + { v4: "ghu_user_access_token_for_github_app" }, + { v5: "ghs_installation_access_token" }, + { v6: "ghr_refresh_token_for_github_app" } + ]; + const after = cs.sanitize(before); + + expect(after).toEqual([{v1: hideValue}, {v2: hideValue}, {v3: hideValue}, {v4: hideValue}, {v5: hideValue}, {v6: hideValue}]); + }); +});