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>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 290dbb4b23e146068be30d521a2baf4e84bbe021 parent 65264c5
2 files changed, +18 −1
  • apps/nim/src/app.nim +7 −1
  • apps/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()