Skip to content

Commit 9b4796f

Browse files
committed
Fix UUID validation and handle server errors for invalid UUID inputs
Fix #65
1 parent 261868b commit 9b4796f

25 files changed

+280
-139
lines changed

CHANGES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ To be released.
1515

1616
- The profile page now shows a user's cover image if they have one.
1717

18+
- Fixed a bug where a server error occurred when an invalid UUID was input via
19+
URL or form data. [[#65]]
20+
21+
[#65]: https://github.com/dahlia/hollo/issues/65
22+
1823

1924
Version 0.3.2
2025
-------------

src/api/v1/accounts.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import {
5757
} from "../../schema";
5858
import { disk, getAssetUrl } from "../../storage";
5959
import { extractCustomEmojis, formatText } from "../../text";
60+
import { type Uuid, isUuid, uuid } from "../../uuid";
6061
import { timelineQuerySchema } from "./timelines";
6162

6263
const app = new Hono<{ Variables: Variables }>();
@@ -265,7 +266,7 @@ app.get(
265266
422,
266267
);
267268
}
268-
const ids = c.req.queries("id[]") ?? [];
269+
const ids = (c.req.queries("id[]") ?? []).filter(isUuid);
269270
const accountList =
270271
ids.length > 0
271272
? await db.query.accounts.findMany({
@@ -448,7 +449,7 @@ app.get(
448449
422,
449450
);
450451
}
451-
const ids: string[] = c.req.queries("id[]") ?? [];
452+
const ids: Uuid[] = (c.req.queries("id[]") ?? []).filter(isUuid);
452453
const result: {
453454
id: string;
454455
accounts: ReturnType<typeof serializeAccount>[];
@@ -488,6 +489,7 @@ app.get(
488489

489490
app.get("/:id", async (c) => {
490491
const id = c.req.param("id");
492+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
491493
const account = await db.query.accounts.findFirst({
492494
where: eq(accounts.id, id),
493495
with: { owner: true, successor: true },
@@ -518,14 +520,15 @@ app.get(
518520
),
519521
),
520522
async (c) => {
523+
const id = c.req.param("id");
524+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
521525
const tokenOwner = c.get("token").accountOwner;
522526
if (tokenOwner == null) {
523527
return c.json(
524528
{ error: "This method requires an authenticated user" },
525529
422,
526530
);
527531
}
528-
const id = c.req.param("id");
529532
const account = await db.query.accounts.findFirst({
530533
where: eq(accounts.id, id),
531534
with: {
@@ -687,14 +690,15 @@ app.post(
687690
tokenRequired,
688691
scopeRequired(["write:follows"]),
689692
async (c) => {
693+
const id = c.req.param("id");
694+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
690695
const owner = c.get("token").accountOwner;
691696
if (owner == null) {
692697
return c.json(
693698
{ error: "This method requires an authenticated user" },
694699
422,
695700
);
696701
}
697-
const id = c.req.param("id");
698702
const following = await db.query.accounts.findFirst({
699703
where: eq(accounts.id, id),
700704
with: { owner: true },
@@ -740,14 +744,15 @@ app.post(
740744
tokenRequired,
741745
scopeRequired(["write:follows"]),
742746
async (c) => {
747+
const id = c.req.param("id");
748+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
743749
const owner = c.get("token").accountOwner;
744750
if (owner == null) {
745751
return c.json(
746752
{ error: "This method requires an authenticated user" },
747753
422,
748754
);
749755
}
750-
const id = c.req.param("id");
751756
const following = await db.query.accounts.findFirst({
752757
where: eq(accounts.id, id),
753758
with: { owner: true },
@@ -782,6 +787,7 @@ app.post(
782787

783788
app.get("/:id/followers", async (c) => {
784789
const accountId = c.req.param("id");
790+
if (!isUuid(accountId)) return c.json({ error: "Record not found" }, 404);
785791
const followers = await db.query.follows.findMany({
786792
where: and(eq(follows.followingId, accountId), isNotNull(follows.approved)),
787793
orderBy: desc(follows.approved),
@@ -801,6 +807,7 @@ app.get("/:id/followers", async (c) => {
801807

802808
app.get("/:id/following", async (c) => {
803809
const accountId = c.req.param("id");
810+
if (!isUuid(accountId)) return c.json({ error: "Record not found" }, 404);
804811
const followers = await db.query.follows.findMany({
805812
where: and(eq(follows.followerId, accountId), isNotNull(follows.approved)),
806813
orderBy: desc(follows.approved),
@@ -823,6 +830,8 @@ app.get(
823830
tokenRequired,
824831
scopeRequired(["read:lists"]),
825832
async (c) => {
833+
const accountId = c.req.param("id");
834+
if (!isUuid(accountId)) return c.json({ error: "Record not found" }, 404);
826835
const owner = c.get("token").accountOwner;
827836
if (owner == null) {
828837
return c.json(
@@ -838,7 +847,7 @@ app.get(
838847
db
839848
.select({ id: listMembers.listId })
840849
.from(listMembers)
841-
.where(eq(listMembers.accountId, c.req.param("id"))),
850+
.where(eq(listMembers.accountId, accountId)),
842851
),
843852
),
844853
});
@@ -853,8 +862,8 @@ app.get(
853862
zValidator(
854863
"query",
855864
z.object({
856-
max_id: z.string().uuid().optional(),
857-
since_id: z.string().uuid().optional(),
865+
max_id: uuid.optional(),
866+
since_id: uuid.optional(),
858867
limit: z
859868
.string()
860869
.default("40")
@@ -911,15 +920,15 @@ app.post(
911920
}),
912921
),
913922
async (c) => {
923+
const id = c.req.param("id");
924+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
914925
const owner = c.get("token").accountOwner;
915-
916926
if (owner == null) {
917927
return c.json(
918928
{ error: "This method requires an authenticated user" },
919929
422,
920930
);
921931
}
922-
const id = c.req.param("id");
923932
const { notifications, duration } = c.req.valid("json");
924933
const account = await db.query.accounts.findFirst({
925934
where: eq(accounts.id, id),
@@ -983,14 +992,15 @@ app.post(
983992
tokenRequired,
984993
scopeRequired(["write:mutes"]),
985994
async (c) => {
995+
const id = c.req.param("id");
996+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
986997
const owner = c.get("token").accountOwner;
987998
if (owner == null) {
988999
return c.json(
9891000
{ error: "This method requires an authenticated user" },
9901001
422,
9911002
);
9921003
}
993-
const id = c.req.param("id");
9941004
await db
9951005
.delete(mutes)
9961006
.where(and(eq(mutes.accountId, owner.id), eq(mutes.mutedAccountId, id)));
@@ -1024,14 +1034,15 @@ app.post(
10241034
tokenRequired,
10251035
scopeRequired(["read:blocks"]),
10261036
async (c) => {
1037+
const id = c.req.param("id");
1038+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
10271039
const owner = c.get("token").accountOwner;
10281040
if (owner == null) {
10291041
return c.json(
10301042
{ error: "This method requires an authenticated user" },
10311043
422,
10321044
);
10331045
}
1034-
const id = c.req.param("id");
10351046
const acct = await db.query.accounts.findFirst({
10361047
where: eq(accounts.id, id),
10371048
with: { owner: true },
@@ -1069,14 +1080,15 @@ app.post(
10691080
tokenRequired,
10701081
scopeRequired(["read:blocks"]),
10711082
async (c) => {
1083+
const id = c.req.param("id");
1084+
if (!isUuid(id)) return c.json({ error: "Record not found" }, 404);
10721085
const owner = c.get("token").accountOwner;
10731086
if (owner == null) {
10741087
return c.json(
10751088
{ error: "This method requires an authenticated user" },
10761089
422,
10771090
);
10781091
}
1079-
const id = c.req.param("id");
10801092
const acct = await db.query.accounts.findFirst({
10811093
where: eq(accounts.id, id),
10821094
with: { owner: true },

src/api/v1/featured_tags.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import {
1010
import type { PgDatabase } from "drizzle-orm/pg-core";
1111
import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js";
1212
import { Hono } from "hono";
13-
import { uuidv7 } from "uuidv7-js";
1413
import { z } from "zod";
1514
import db from "../../db";
1615
import { serializeFeaturedTag } from "../../entities/tag";
1716
import { type Variables, scopeRequired, tokenRequired } from "../../oauth";
1817
import type * as schema from "../../schema";
1918
import { featuredTags, posts } from "../../schema";
19+
import { type Uuid, isUuid, uuidv7 } from "../../uuid";
2020

2121
const app = new Hono<{ Variables: Variables }>();
2222

@@ -65,6 +65,10 @@ app.delete(
6565
tokenRequired,
6666
scopeRequired(["write:accounts"]),
6767
async (c) => {
68+
const featuredTagId = c.req.param("id");
69+
if (!isUuid(featuredTagId)) {
70+
return c.json({ error: "Record not found" }, 404);
71+
}
6872
const owner = c.get("token").accountOwner;
6973
if (owner == null) {
7074
return c.json({ error: "The access token is invalid." }, 401);
@@ -74,7 +78,7 @@ app.delete(
7478
.where(
7579
and(
7680
eq(featuredTags.accountOwnerId, owner.id),
77-
eq(featuredTags.id, c.req.param("id")),
81+
eq(featuredTags.id, featuredTagId),
7882
),
7983
)
8084
.returning();
@@ -89,7 +93,7 @@ async function getFeaturedTagStats(
8993
typeof schema,
9094
ExtractTablesWithRelations<typeof schema>
9195
>,
92-
ownerId: string,
96+
ownerId: Uuid,
9397
): Promise<Record<string, { posts: number; lastPublished: Date | null }>> {
9498
const result = await db
9599
.select({

src/api/v1/follow_requests.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { federation } from "../../federation";
1111
import { updateAccountStats } from "../../federation/account";
1212
import { type Variables, scopeRequired, tokenRequired } from "../../oauth";
1313
import { accounts, blocks, follows, mutes } from "../../schema";
14+
import { isUuid } from "../../uuid";
1415

1516
const app = new Hono<{ Variables: Variables }>();
1617

@@ -40,14 +41,15 @@ app.post(
4041
tokenRequired,
4142
scopeRequired(["write:follows"]),
4243
async (c) => {
44+
const followerId = c.req.param("account_id");
45+
if (!isUuid(followerId)) return c.json({ error: "Record not found" }, 404);
4346
const owner = c.get("token").accountOwner;
4447
if (owner == null) {
4548
return c.json(
4649
{ error: "This method requires an authenticated user" },
4750
422,
4851
);
4952
}
50-
const followerId = c.req.param("account_id");
5153
const follower = await db.query.accounts.findFirst({
5254
where: eq(accounts.id, followerId),
5355
with: { owner: true },
@@ -113,14 +115,15 @@ app.post(
113115
tokenRequired,
114116
scopeRequired(["write:follows"]),
115117
async (c) => {
118+
const followerId = c.req.param("account_id");
119+
if (!isUuid(followerId)) return c.json({ error: "Record not found" }, 404);
116120
const owner = c.get("token").accountOwner;
117121
if (owner == null) {
118122
return c.json(
119123
{ error: "This method requires an authenticated user" },
120124
422,
121125
);
122126
}
123-
const followerId = c.req.param("account_id");
124127
const follower = await db.query.accounts.findFirst({
125128
where: eq(accounts.id, followerId),
126129
with: { owner: true },

0 commit comments

Comments
 (0)