Skip to content

Commit b0100ca

Browse files
authored
fix(core): validate username / gist id against the safe-character pattern (#356)
- Related to #342 `stats` and `pin` already reject `username`/`repo`/`owner` that don't match `safePattern` (`/^[-\w/.,]+$/`), but `top-langs`, `wakatime`, and `gist` didn't. This applies the same guard: - `top-langs` + `wakatime`: reject a `username` containing unsafe characters. Matters most for `wakatime`, where `username` is interpolated into the outbound request URL path. - `gist`: same guard on its `id` param. All three return the existing `error - permanent` card before any fetch. ## Additional change Simplified the `layout` / `stats_format` checks in `top-langs`: dropped the redundant `typeof x !== "string" ||` (a non-string already fails `.includes(...)`), keeping the `x !== undefined` guard so the params stay optional.
1 parent 7c7fca4 commit b0100ca

4 files changed

Lines changed: 86 additions & 4 deletions

File tree

packages/core/src/api/gist.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,24 @@ export default async (
4343
};
4444
}
4545

46+
const safePattern = /^[-\w/.,]+$/;
47+
if (id && !safePattern.test(id)) {
48+
return {
49+
status: "error - permanent",
50+
content: renderError({
51+
message: "Something went wrong",
52+
secondaryMessage: "Gist ID contains unsafe characters",
53+
renderOptions: {
54+
title_color,
55+
text_color,
56+
bg_color,
57+
border_color,
58+
theme,
59+
},
60+
}),
61+
};
62+
}
63+
4664
try {
4765
const gistData = await fetchGist(id, pat);
4866

packages/core/src/api/top-langs.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,27 @@ export default async (
5555
};
5656
}
5757

58+
const safePattern = /^[-\w/.,]+$/;
59+
if (username && !safePattern.test(username)) {
60+
return {
61+
status: "error - permanent",
62+
content: renderError({
63+
message: "Something went wrong",
64+
secondaryMessage: "Username contains unsafe characters",
65+
renderOptions: {
66+
title_color,
67+
text_color,
68+
bg_color,
69+
border_color,
70+
theme,
71+
},
72+
}),
73+
};
74+
}
75+
5876
if (
5977
layout !== undefined &&
60-
(typeof layout !== "string" ||
61-
!["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout))
78+
!["compact", "normal", "donut", "donut-vertical", "pie"].includes(layout)
6279
) {
6380
return {
6481
status: "error - permanent",
@@ -78,8 +95,7 @@ export default async (
7895

7996
if (
8097
stats_format !== undefined &&
81-
(typeof stats_format !== "string" ||
82-
!["bytes", "percentages"].includes(stats_format))
98+
!["bytes", "percentages"].includes(stats_format)
8399
) {
84100
return {
85101
status: "error - permanent",

packages/core/src/api/wakatime.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,24 @@ export default async ({
4949
};
5050
}
5151

52+
const safePattern = /^[-\w/.,]+$/;
53+
if (username && !safePattern.test(username)) {
54+
return {
55+
status: "error - permanent",
56+
content: renderError({
57+
message: "Something went wrong",
58+
secondaryMessage: "Username contains unsafe characters",
59+
renderOptions: {
60+
title_color,
61+
text_color,
62+
bg_color,
63+
border_color,
64+
theme,
65+
},
66+
}),
67+
};
68+
}
69+
5270
try {
5371
const stats = await fetchWakatimeStats({ username, api_domain });
5472

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import gistApi from "../src/api/gist.js";
4+
import statsApi from "../src/api/index.js";
5+
import pinApi from "../src/api/pin.js";
6+
import topLangsApi from "../src/api/top-langs.js";
7+
import wakatimeApi from "../src/api/wakatime.js";
8+
9+
// Values containing characters outside the safe set /^[-\w/.,]+$/. These must be
10+
// rejected before any network request is made.
11+
const unsafeValues = ["user name", "user@evil.com", "a<b", "a?b", "a:b", "a&b"];
12+
13+
describe("API input validation", () => {
14+
describe.each([
15+
["top-langs", "username", topLangsApi],
16+
["wakatime", "username", wakatimeApi],
17+
["gist", "id", gistApi],
18+
["stats", "username", statsApi],
19+
["stats", "repo", statsApi],
20+
["stats", "owner", statsApi],
21+
["pin", "username", pinApi],
22+
["pin", "repo", pinApi],
23+
])("%s: %s", (_endpoint, param, api) => {
24+
it.each(unsafeValues)(`rejects unsafe ${param} %j`, async (value) => {
25+
const result = await api({ [param]: value });
26+
expect(result.status).toBe("error - permanent");
27+
expect(result.content).toContain("unsafe characters");
28+
});
29+
});
30+
});

0 commit comments

Comments
 (0)