diff --git a/packages/core/src/api/gist.js b/packages/core/src/api/gist.js index 0cb9610262ca8..ab35c654bfaeb 100644 --- a/packages/core/src/api/gist.js +++ b/packages/core/src/api/gist.js @@ -43,6 +43,24 @@ export default async ( }; } + const safePattern = /^[-\w/.,]+$/; + if (id && !safePattern.test(id)) { + return { + status: "error - permanent", + content: renderError({ + message: "Something went wrong", + secondaryMessage: "Gist ID contains unsafe characters", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + }; + } + try { const gistData = await fetchGist(id, pat); diff --git a/packages/core/src/api/top-langs.js b/packages/core/src/api/top-langs.js index 46f6633c013e6..2ebc6218718df 100644 --- a/packages/core/src/api/top-langs.js +++ b/packages/core/src/api/top-langs.js @@ -55,10 +55,27 @@ export default async ( }; } + const safePattern = /^[-\w/.,]+$/; + if (username && !safePattern.test(username)) { + return { + status: "error - permanent", + content: renderError({ + message: "Something went wrong", + secondaryMessage: "Username contains unsafe characters", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + }; + } + if ( layout !== undefined && - (typeof layout !== "string" || - !["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout)) + !["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout) ) { return { status: "error - permanent", @@ -78,8 +95,7 @@ export default async ( if ( stats_format !== undefined && - (typeof stats_format !== "string" || - !["bytes", "percentages"].includes(stats_format)) + !["bytes", "percentages"].includes(stats_format) ) { return { status: "error - permanent", diff --git a/packages/core/src/api/wakatime.js b/packages/core/src/api/wakatime.js index 19307f104aaf8..d9333f49a926c 100644 --- a/packages/core/src/api/wakatime.js +++ b/packages/core/src/api/wakatime.js @@ -49,6 +49,24 @@ export default async ({ }; } + const safePattern = /^[-\w/.,]+$/; + if (username && !safePattern.test(username)) { + return { + status: "error - permanent", + content: renderError({ + message: "Something went wrong", + secondaryMessage: "Username contains unsafe characters", + renderOptions: { + title_color, + text_color, + bg_color, + border_color, + theme, + }, + }), + }; + } + try { const stats = await fetchWakatimeStats({ username, api_domain }); diff --git a/packages/core/tests/apiInputValidation.test.js b/packages/core/tests/apiInputValidation.test.js new file mode 100644 index 0000000000000..5ccdee180c038 --- /dev/null +++ b/packages/core/tests/apiInputValidation.test.js @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; + +import gistApi from "../src/api/gist.js"; +import statsApi from "../src/api/index.js"; +import pinApi from "../src/api/pin.js"; +import topLangsApi from "../src/api/top-langs.js"; +import wakatimeApi from "../src/api/wakatime.js"; + +// Values containing characters outside the safe set /^[-\w/.,]+$/. These must be +// rejected before any network request is made. +const unsafeValues = ["user name", "user@evil.com", "a { + describe.each([ + ["top-langs", "username", topLangsApi], + ["wakatime", "username", wakatimeApi], + ["gist", "id", gistApi], + ["stats", "username", statsApi], + ["stats", "repo", statsApi], + ["stats", "owner", statsApi], + ["pin", "username", pinApi], + ["pin", "repo", pinApi], + ])("%s: %s", (_endpoint, param, api) => { + it.each(unsafeValues)(`rejects unsafe ${param} %j`, async (value) => { + const result = await api({ [param]: value }); + expect(result.status).toBe("error - permanent"); + expect(result.content).toContain("unsafe characters"); + }); + }); +});