Shrnutí
Na webu motokeska.cz byl proveden cílený bezpečnostní audit, a to na základě výslovného souhlasu správce systému. Audit probíhal bez jakýchkoli zvláštních oprávnění a simuloval reálného útočníka bez přístupu zevnitř.
Napříč čtyřmi různými plochami útoku bylo identifikováno pět zranitelností:
- Voucher kódy jsou vydávány ještě před dokončením platby, což umožňuje generovat neomezené množství voucherů zdarma
- Race condition v endpointu pro uplatnění voucheru umožňuje jedním voucherem aktivovat více tras
- Tokeny z QR keší jsou dostupné přes veřejné API, což umožňuje podvod se soutěží o ceny bez fyzické návštěvy míst
- Server přijímá nahrání libovolného souboru kvůli nedostatečné validaci obsahu
- Uživatelské profilové fotky si ponechávají EXIF metadata, která mohou prozradit GPS souřadnice

Všechny nálezy byly zodpovědně nahlášeny vlastníkovi domény, který chyby potvrdil a udělil souhlas s publikováním tohoto článku.
Vouchery zdarma: jak přeskočit platbu
Popis
Motokeska.cz umožňuje uživatelům zakoupit vouchery, které odemykají placené trasy, a to na stránce nákupu voucherů.
Nákupní proces je přímočarý – uživatel vyplní e-mail, zvolí typ voucheru a přejde k platbě přes bránu GoPay.

V čem je problém? Server vygeneruje a vrátí platný voucher kód ještě před dokončením platby.
Při zahájení nákupu prohlížeč volá následující API endpoint:
POST /api/payments/coupon-payment HTTP/2
Host: api-dot-motokeska.ew.r.appspot.com
Content-Type: application/json
{"email":"hacker@evil.com","coupon_type":"single","year":2026}
Server okamžitě odpoví plně platným voucher kódem:
HTTP/2 200 OK
Content-Type: application/json
{
"gw_url":"https://gate.gopay.com/gw-ui/rest/v3/bf312f244c524733b673a9ee4c066000",
"id":1234567890,
"coupon_code":"MOTO-ABCD123"
}
Pole coupon_code v odpovědi je okamžitě uplatnitelné – bez ohledu na to, zda uživatel platbu vůbec kdy dokončí. URL platební brány (gw_url) je v odpovědi přítomné, ale server před vydáním kódu nikdy nečeká na potvrzovací callback od GoPay. Generování voucheru a platba jsou tak od sebe fakticky oddělené.
To znamená, že tento endpoint lze volat opakovaně a generovat neomezené množství platných voucherů zdarma.
Náprava
Voucher kódy by měly být generovány až po přijetí ověřeného potvrzení o platbě (callbacku) z platební brány. Server nesmí nikdy zahrnout voucher kód do počáteční odpovědi při vytvoření platby – kód by měl být vydán teprve poté, co je úspěšná platba potvrzena na straně serveru.
Jeden voucher, neomezeně tras: zneužití race condition
Popis
Motokeska.cz umožňuje přihlášeným uživatelům uplatňovat voucher kódy přes jejich profilovou stránku. Každý voucher je zamýšlen jako jednorázový a měl by odemknout přesně jednu placenou trasu.

Uplatnění obsluhuje endpoint /api/coupons/redeem. Standardní požadavek vypadá takto:
POST /api/coupons/redeem HTTP/2
Host: api-dot-motokeska.ew.r.appspot.com
Authorization: Bearer <JWT>
{"code":"MOTO-ABCD123","voucher_type":"single","selected_route":{"year":2026,"type":"main","number":"05"}}
Pokud je stejný voucher uplatněn sekvenčně na druhou trasu, server jej správně odmítne:
Tento voucher byl již použit. Každý voucher lze uplatnit pouze jednou.
Pokud je ale odesláno více požadavků na uplatnění souběžně – každý cílící na jinou trasu úpravou parametru selected_route.number – dostanou se všechny požadavky na server dřív, než je voucher označen jako použitý. Každý požadavek uspěje nezávisle a odemkne jinou placenou trasu se stejným voucher kódem.

Náprava
Endpoint pro uplatnění musí voucher ověřit a označit jako použitý v jediné atomické operaci – ne ve dvou samostatných krocích. Transakce na úrovni databáze s pesimistickým zámkem (např. SELECT FOR UPDATE) zajistí, že jakmile první požadavek přečte stav voucheru, žádný souběžný požadavek jej nemůže přečíst, dokud první požadavek nedokončí zápis. Každý požadavek, který dorazí v době, kdy je zámek držen, by měl dostat chybovou odpověď, nikoli úspěch.
Dokonči trasu bez opuštění domova: jak podvádět ve hře přes API
Popis
Motokeska je hra v reálném světě. Hráči zakoupí trasu, dojedou (autem či na motorce) k fyzickým místům a naskenují QR kódy zabudované v kovových štítcích rozmístěných po krajině. Takový štítek vypadá takto:

Naskenování QR kódu na takovém štítku přesměruje hráče na URL ve formátu:
https://motokeska.cz/keska/2025/city/01/14?token=JyOz6oe4gC602nRCW2bV
Parametr token je to, co dokazuje, že jste byli fyzicky na daném místě. Je to hlavní herní mechanika a vstupenka do slosování o ceny.
Problém je, že tyto tokeny jsou dostupné přes veřejný API endpoint. Kdokoli je může získat, aniž by opustil svůj stůl.
Následující požadavek vrátí všechna data o keších pro danou trasu, včetně hodnot token, které jsou normálně získatelné pouze fyzickou návštěvou místa a naskenováním štítku:
GET /api/caches?route_id=12&year=2026 HTTP/2
Host: api-dot-motokeska.ew.r.appspot.com
Authorization: Bearer <JWT>
Odpověď obsahuje token pro každou keš na trase:
HTTP/2 200 OK
Content-Type: application/json
[
{
"id": 42,
"route_id": 12,
"name": "Cache #1",
"qr_code_token": "JyOz6oe4gC602nRCW2bV",
"gps_lat": 49.8175,
"gps_lon": 13.4734
},
...
]
S těmito tokeny v ruce může útočník sestavit platné požadavky na potvrzení keše a odeslat je, jako by každé místo navštívil – dokončit celou trasu z domova, získat body a zúčastnit se slosování o ceny, aniž by kdy nastartoval motor.

Náprava
Pole qr_code_token nesmí být nikdy vráceno přes API dříve, než byla keš fyzicky naskenována. Token by měl být použit pouze na straně serveru k ověření příchozího požadavku o naskenování – nemá žádný legitimní důvod být odesílán klientovi předem.
Vaše profilová fotka může prozradit vaši polohu
Popis
Motokeska.cz umožňuje uživatelům nastavit si profilovou fotku. Co by většina uživatelů nečekala, je to, že jejich fotka může nenápadně prozradit, kde bydlí.
Když uživatel nahraje profilovou fotku, obrázek se uloží do veřejně přístupného Google Cloud Storage bucketu bez jakékoli sanitizace metadat. To znamená, že pokud nahraná fotka obsahuje EXIF metadata – jako většina fotek z chytrých telefonů – tato metadata zůstanou zachována a jsou dostupná komukoli, kdo si soubor stáhne.
Bucket lze procházet přímo přes storage endpoint, aniž by se aplikace vůbec použila:
https://storage.googleapis.com/motokeska-user-images?prefix=profile-images
To vrátí seznam všech uložených objektů, které lze následně hromadně stáhnout:
burl='https://storage.googleapis.com/motokeska-user-images/'
curl -s https://storage.googleapis.com/motokeska-user-images \
| rg -oPN '(?<=<Key>).*?(?=</Key>)' > keys.list
for key in $(cat keys.list); do
curl -s -O "${burl}${key}"
done
Některé z nahraných fotek:
![]()
Po stažení lze obrázky prozkoumat nástrojem exiftool. Pokud autor před nahráním metadata neodstranil, může fotka prozradit místo, kde byla pořízena. Příklad z iPhonu:
[ExifIFD] LensModel : iPhone 14 Pro front TrueDepth camera 2.69mm f/1.9
[GPS] GPSLatitude : 50 deg 5' 11.04"
[GPS] GPSLongitude : 14 deg 24' 35.88"
[GPS] GPSAltitude : 187.0396476 m
Selfie pořízené doma a použité jako profilová fotka právě prozradilo domácí adresu uživatele komukoli, kdo ví, kde hledat.

Náprava
Z nahraných obrázků by měla být před uložením odstraněna veškerá metadata. To lze provést na straně serveru pomocí nástrojů jako exiftool, ImageMagick, nebo přeuložením (re-encode) obrázku přes knihovnu pro zpracování obrázků – což zároveň slouží jako další vrstva obrany proti poškozeným či škodlivým nahráním.
Nahrát cokoli: když server věří příliš
Popis
Motokeska.cz umožňuje uživatelům nahrát profilovou fotku přes jejich profilovou stránku. Server má přijímat pouze obrázky – jenže validaci lze triviálně obejít.
Endpoint pro nahrávání kontroluje pouze hlavičku Content-Type požadavku. Pokud hlavička začíná na image/, je soubor přijat – bez ohledu na to, co soubor ve skutečnosti obsahuje. Útočník může nahrát jakýkoli soubor prostým nastavením hlavičky na image/jpeg, zatímco odešle úplně jiná data:
POST /api/users/123/profile-image HTTP/2
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
------WebKitFormBoundary
Content-Disposition: form-data; name="profileImage"; filename="image.png"
Content-Type: image/javascript
Server nahrání přijme a uloží soubor do veřejně přístupného Google Cloud Storage bucketu s příponou odvozenou z dodaného Content-Type – nikoli ze skutečného obsahu souboru.
Ve stejném endpointu se skrývá ještě druhý problém. Název souboru uloženého v bucketu je odvozen z uživatelského ID v cestě požadavku. Přidání úvodních nul k uživatelskému ID vytvoří jiný název souboru, přičemž server stále požadavek přijme jako platný:
| Cesta požadavku | Uložený název souboru |
|---|---|
/api/users/3/profile-image |
user_3.jpg |
/api/users/003/profile-image |
user_003.jpg |
/api/users/00000003/profile-image |
user_00000003.jpg |
To znamená, že útočník může nahrát neomezené množství souborů do důvěryhodného Google Cloud Storage bucketu – bucketu, který může být zařazen na whitelist jinými systémy nebo CSP politikami – jednoduše zvyšováním počtu úvodních nul.
Náprava
Hlavičky Content-Type jsou pod kontrolou uživatele a nesmí být nikdy důvěryhodné jako jediný validační mechanismus. Server by měl prozkoumat skutečný obsah souboru pomocí validace magic bytes a odmítnout cokoli, co neodpovídá známému formátu obrázku. Identifikátor uživatele v cestě pro nahrání musí být před použitím normalizován – úvodní nuly by měly být odstraněny a výsledné ID ověřeno vůči přihlášenému uživateli. Vynucení jednoho aktivního profilového obrázku na uživatele nahrazováním existujících souborů namísto vytváření nových by rovněž odstranilo vektor zneužití úložiště.
Časová osa zveřejnění
9. března 2026 – požádáno o svolení vlastníka motokeska.cz (David Král) k provedení bezpečnostního testování
27. března 2026 – podmínky přezkoumány a upřesněny
6. června 2026 – osobní schůzka, vysvětlení, doporučení
7. června 2026 – report zaslán vlastníkovi motokeska.cz
9. června 2026 – článek publikován
Odměna
Pro motokeska.cz neexistuje žádný bug bounty program a o žádnou odměnu nebylo žádáno. Report byl sdílen dobrovolně. Tým motokeska.cz zcela z vlastní iniciativy zaslal odměnu ve výši 20 000 Kč.