Repository navigation
feat(apps): signed launches, strict mode, verifiable payments and install counts; fix roaming CSRF - #1848
Open
qpprt wants to merge 5 commits into
Open
feat(apps): signed launches, strict mode, verifiable payments and install counts; fix roaming CSRF#1848qpprt wants to merge 5 commits into
qpprt wants to merge 5 commits into
Conversation
Both authenticate with the session cookie alone. The roaming check passes when Referer is missing, so any website could call /method/*?auth_mechanism=roaming on behalf of the visitor by stripping it, and /rpc (which also runs Apps.pay) had no check at all. Sec-Fetch-Site and Sec-Fetch-Mode are set by the browser and can't be changed by a page. OpenVK's own pages call both with fetch() from the same origin, so cross-site and same-site requests and navigations are refused: a link opened from an email or another app is sent with Sec-Fetch-Site: none, and a link inside a post is same-origin. Clients that don't send these headers aren't browsers and aren't affected.
added 4 commits
October 10, 2026 15:52
- per-app secret (lazy), shown on the edit page with a regenerate button - strict mode (default for new apps): signed ovk_* launch params in the iframe URL and a bridge locked to the app origin - getOrigin() omits default ports so it matches event.origin - bridge replies go to event.origin instead of '*' - Apps.pay is now transactional and logged to app_payments - new Apps.payOrder / VKAPI.buyOrder: idempotent by order id, returns a signed receipt - VKAPI.getLaunchParams() in GameAPI.js - Apps.withdrawFunds empties the balance under a row lock in one transaction, so a payment arriving meanwhile isn't lost - Apps.* rejects pass an error code (they surfaced as "Invalid params") - strict mode refuses addresses with ovk_* query keys, also percent-encoded - order ids are compared exactly (ascii_bin): Order-A and order-a are different orders, and a trailing newline is refused - a withdrawal locks the owner's balance before the app's, in the same order as payments, so an owner paying their own app can't deadlock it
PaymentRequest description and outSum come from the app iframe and were inserted into MessageBox as HTML, so any app could run scripts on OpenVK pages.
apps.installs is a counter kept next to app_users: install and uninstall change the row and the counter in one transaction, and only if the row really changed. INSERT IGNORE also fixes opening an app in two tabs at once, which could insert the same row twice and fail. The migration fills the counter for existing apps from app_users and adds an index, so the catalog sorted by installs is an index read instead of a COUNT(*) per app. With 700 apps and 2.2M installs the migration takes about a second and the catalog page under 1 ms (counting on the fly: ~660 ms). The catalog (search, apps section) now defaults to sorting by installs and shows the count under each app; creation date is still available.
/app12#room_5 opens the app with #room_5 on its own address, like vk.com/app123#... did: apps can link to a room, a level or an invite. The value is whatever the user's link says, so it isn't signed or limited; apps read it from location.hash as user input. The frame's address is now set from the page script, once: a strict mode launch link works only once, so the frame must not load twice.
Collaborator
|
wow ogo |
26 of 33 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Apps today can't be built safely on OpenVK:
VKAPI.getUser()in the page, which the page itself controls, so any backend that trusts it can be fooled.pay()returns a signature made with the instance secret, which apps don't have. There is no order id, so a lost response can't be retried safely. The balance is changed with a read-modify-write, so parallel payments can overspend.NaN/INFamounts aren't rejected.'*'. If the frame navigates to another page, that page receives the user's info and payment results.Application::getOrigin()always appends:443, so it never equals a browser origin.Separately, the API's
roamingmode (used by the app bridge to call API methods as the logged-in user) only checksReferer, and only when one is sent. A page on another site can drop theRefererand make the user's browser call/method/...?auth_mechanism=roaming: CSRF on the whole API.What changes
1.
fix(api): roaming accepts only same-origin requestsRequests with
Sec-Fetch-Site: cross-siteorsame-site, or withSec-Fetch-Mode: navigate, are refused. Browsers set these headers themselves and pages can't change them. Non-browser clients don't use roaming, and the existing Referer check stays as is.2.
feat(apps): strict mode, a secret per app, signed launches and paymentsovk_app_id, ovk_user_id, ovk_ts, ovk_launch_id, ovk_type=launch, ovk_sign. The signature is HMAC-SHA256 with the app's secret, the same scheme VK Mini Apps use, so the app's backend can trust the user id.https://and must not containovk_*parameters.VKAPI.buyOrder(price, title, orderId)returns a signed receipt (ovk_order_id, ovk_payment_id, ovk_amount, ...) the backend can verify.duplicate: trueand doesn't charge twice. A unique(app, order_id)key makes this safe under concurrency.SELECT ... FOR UPDATE) and both balances are updated atomically.NaN/INFamounts are refused.'*'.GameAPI.js: addsgetLaunchParams()andbuyOrder(); the bridge listener now checks the reply's origin.getOrigin(): returns a real origin (lowercase, default port omitted).00070-apps-secrets-and-payments.sql:apps.secret,apps.strict, and theapp_paymentstable.3.
fix(apps): escape app-supplied text in bridge dialogsCovers the payment dialog (app title, description, amount) and error messages.
4.
feat(apps): install counts, catalog sorted by installsapps.installs, kept next toapp_users. Install and uninstall change the row and the counter in one transaction, and only if the row really changed.INSERT IGNOREalso fixes opening an app in two tabs at once, which could insert the same row twice and fail.00071-apps-installs.sqlfills the counter fromapp_usersand adds an index, so sorting by installs is an index read instead of aCOUNT(*)per app.5.
feat(apps): pass the part of the link after#to the app/app12#room_5opens the app with#room_5on its own address, likevk.com/app123#...did, so apps can link to a room, a level or an invite. The value is whatever the user's link says, so it isn't signed or limited; apps read it fromlocation.hashas user input. The frame's address is now set from the page script, exactly once, because a strict-mode launch link works only once.Compatibility
buy()and the oldpay()still work, and are now transactional.00071runs in 0.9 s and fills 700/700 counts correctly. A catalog page then takes 0.04 ms; counting on the fly takes ~660 ms.Testing
php-cs-fixer --dry-run(whole repo),chandler-latte-lint(211 + 4 templates) and PHPStan (CI config) are clean.master's database: clean.mastervs this branch, with a debug app (94 checks):masterand is refused here.tests/), on the last commit (4c9e12a3): 82 passed, 1 skipped.#:/app2#room_xlands in that room on a full page load, through OpenVK's in-site navigation, and from a link in a wall post; a plain/app2is unchanged. The 94-check suite was rerun on this commit, including legacy apps: all pass.buyOrder, multiplayer, and friends leaderboards. They have their own browser tests.Try it
https://dev.ovk.app runs this branch.
ovk_*parameters; the shop's "Турбо" goes throughbuyOrderand the confirmation dialog./app2#room_…) opens the same room for a friend.Docs
A developer docs page for apps is ready for OpenVK/docs (
openvk_engine/game_api.md, which is currently "todo") and will be submitted there separately. They cover creating an app, strict mode, launch parameters, checking signatures (PHP, Node and Python examples, tested against real launches), the bridge methods, the order/receipt flow and error codes, and links with#.Not in this PR
Follow-ups that are noted but kept out to keep this focused:
withdrawCoins()uses a read-modify-write like the oldpay()did;$reject("...")calls pass no error code;SameSite=Lax, a separate Chandler PR.Screenshots
App edit page: strict mode and secret key

Payment confirmation (

buyOrder) from a gameCatalog sorted by installs
