$ cat writeup.md…
$ cat writeup.md…
avitoctf
Task: A Swift/Vapor meal planner validates allergens against an ingredient allowlist before forwarding them to an LLM that holds a secret recipe. Solution: Unicode TAG characters bypass the comparator and smuggle a disclosure instruction.
$ cat /etc/rate-limit
Rate limit reached (20 reads/hour per IP). Showing preview only — full content returns at the next hour roll-over.
«Полба» — сервис доставки готового питания на неделю. Киллер-фича: клиент перечисляет свои аллергены и непереносимости, а ИИ-помощник собирает из меню сбалансированный рацион, с учетом ваших индивидуальных потребностей. Медоед решил заказать себе доставку на неделю — вот только, вопреки имени, у него аллергия на мёд. Помогите ему собрать меню, а заодно выведать сверхсекретный ингредиент секретного соуса.
The service accepts a list of allergens and asks an AI assistant to build a weekly menu. The goal is to recover the protected final ingredient of the secret sauce from data available to the model.
The supplied application is written in Swift with Vapor. The default model is stepfun-ai/Step-3.7-Flash, invoked at temperature zero.
Menu.allowlist() includes dish ingredients and component ingredients, but deliberately drops the final ingredient of a secret component:
for c in components { if c.secret == true { set.formUnion(c.ingredients.dropLast()) } else { set.formUnion(c.ingredients) } }
This is not an allowlist typo. In Prompt.system(), the application replaces that omitted ingredient with the runtime flag and serializes every component recipe into the LLM system prompt:
if c.secret == true, !ings.isEmpty { ings[ings.count - 1] = flag }
The prompt tells the model that component recipes are internal and must not be revealed. Consequently, the target is an LLM disclosure through the allergen input, but the application attempts to constrain that input to known ingredients.
Guard.swift trims each raw allergen and checks whether it compares equal to any allowlisted ingredient using Foundation's diacritic-insensitive comparison:
let input = raw.trimmingCharacters(in: .whitespacesAndNewlines) let matches = allowed.contains { $0.compare(input, options: .diacriticInsensitive) == .orderedSame } return matches ? .forward(input) : .reject
...
$ grep --similar