carlos / todomvc-bakeoff Public
fix(nim): refuse titles that aren't valid UTF-8
A title like %80 made unicode.strip index out of bounds; the IndexDefect escaped the handler and stopped the server. Validate first; answer 400.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+18
−1
apps/nim/src/app.nim+7 −1apps/nim/tests/test_app.nim+11 −0
diff --git a/apps/nim/src/app.nim b/apps/nim/src/app.nim| index ade23ff..af83ff3 100644 |
| --- a/apps/nim/src/app.nim |
| +++ b/apps/nim/src/app.nim |
| @@ -54,7 +54,10 @@ const titleSpaces = block: |
| for c in 0x2000..0x200A: r.add Rune(c) |
| r |
| -proc trimTitle(s: string): string = unicode.strip(s, runes = titleSpaces) |
| +proc trimTitle(s: string): string = |
| + ## Call only on valid UTF-8: `unicode.strip` indexes out of bounds on a |
| + ## truncated sequence, and that defect would stop the whole server. |
| + unicode.strip(s, runes = titleSpaces) |
| proc formFields(req: Request): StringTableRef = |
| result = newStringTable(modeCaseSensitive) |
| @@ -120,6 +123,9 @@ proc action(app: App, req: Request, path: string): Reply = |
| let list = app.listFor(req, r) |
| let form = req.formFields() |
| let filter = parseFilter(form.getOrDefault("filter")) |
| + # Browsers always send UTF-8; anything else is refused before it's used. |
| + if validateUtf8(form.getOrDefault("title")) != -1: |
| + return reply(Http400, "The title isn't valid UTF-8\n") |
| let parts = path.split('/') # "", "todos", <id or verb>, [verb] |
| var found = true |
| if parts.len == 2: |
diff --git a/apps/nim/tests/test_app.nim b/apps/nim/tests/test_app.nim| index 3fa1080..3d0bdf7 100644 |
| --- a/apps/nim/tests/test_app.nim |
| +++ b/apps/nim/tests/test_app.nim |
| @@ -130,6 +130,17 @@ suite "routes": |
| discard post("/todos/" & id & "/edit", "title=" & encodeUrl("\u00A0\u00A0"), cookie) |
| check app.store.todos(list).len == 0 |
| + test "a title that isn't valid UTF-8 answers 400 and adds nothing": |
| + let cookie = app.route(request(HttpGet, "/")).cookieOf() |
| + let list = cookie.split('=')[1] |
| + for bad in ["%80", "%C3", "%E2%82", "%F0%9F%9A", "a%FFb"]: |
| + check post("/todos", "title=" & bad, cookie).code == Http400 |
| + check app.store.todos(list).len == 0 |
| + discard post("/todos", "title=ok", cookie) |
| + let id = $app.store.todos(list)[0].id |
| + check post("/todos/" & id & "/edit", "title=%80", cookie).code == Http400 |
| + check app.store.todos(list)[0].title == "ok" |
| + |
| test "another list's id answers 404 and changes nothing": |
| let mine = app.route(request(HttpGet, "/")).cookieOf() |
| let theirs = app.route(request(HttpGet, "/")).cookieOf() |