VISH app — RUT (MIIT) timetable

The timetable of the Russian University of Transport for students and staff of every institute. The timetable of your own group and of any teacher by day and week, free MIIT rooms right now, occupancy of rooms, groups and teachers, notifications about timetable changes and an AI timetable assistant.

Institutes

The RUT (MIIT) app covers five institutes: VISH — Higher Engineering School, AGA — Civil Aviation Academy, ADKH — Road Management Academy, PISH — Advanced Engineering School “HSR Academy” and IMTK — Institute of International Transport Communications. A group’s and a teacher’s timetable are searched the same way in any of them.

What the VISH app can do
Sections
Guide

Integration security

Updated 27.10.2026

What to do on your side so that integration with VISH does not become a hole: it contains keys, tokens, and student data. The list is checked against how it works on October VISH, 2026, and with the recommendations of OAuth (RFC 9700).

API Keys

  • The key is only on the server. Do not insert it into the page code, mobile application, or browser extension: anyone can get it from there. If the program is running for the user, it does not need your key, but an “entrance through VISH”.
  • Keep it in secrets, not in a repository: environment variable, secret repository (Lockbox, Vault, CI secrets). The key starts with vish_. Using this prefix, it is easy to find it by searching for leaks.
  • Minimum rights. When releasing, mark only what the program needs. You don't need a key at all to read public data.
  • Term. Set a reasonable end date; a self-issued key is required (no longer than 730 days).
  • If it lights up, re—release it with the Re-Release button on the “developer” page: the old line stops working immediately.
  • Не пишите в журналы заголовки X-Api-Key и Authorization, адреса с параметрами и тела ответов с личными данными.

“Log in via VISH”

  • PKCE S256 is always available, and for closed applications too. VISH other options and does not accept.
  • state — random, your own for each input; check it in the return address before any other actions. This is a protection against login forgery (login CSRF).
  • nonce is random, check from nonce to id_token: this way the ID token cannot be slipped from someone else's input.
  • iss в адресе возврата должен совпадать с issuer из документа настройки — защита от путаницы между провайдерами (mix-up).
  • The exact return addresses. Register exactly the addresses you are returning to; VISH compares them character by character and does not allow *. Do not make the return address a “universal redirector”.
  • Check id_token: iss, aud — your client_id, nonce, exp. Signature (ES256) — using keys out of jwks_uri, if the token did not come directly from the /oauth/token response.
  • Минимум разрешений. Просите только нужные scope: человеку показывают весь список, и длинный список — повод отказать.
  • The way out is with a review. When a person logs out of your app, revoke the refresh token after POST /oauth/revoke.

Where to store tokens

  • The tokens are not in the address. Neither access_token nor refresh_token should be included in the page address, Referer or the browser history. VISH it issues them only in the response body and rejects the codes and tokens in the protocol request address.
  • Best of all, on the server (backend-for-frontend scheme): the browser only holds session cookies for your site with flags HttpOnly, Secure, SameSite=Lax, and tokens VISH are stored on the server.
  • If it's just the browser, keep the tokens in the page's memory, and code_verifier and state in sessionStorage for the login time. Do not put tokens in localStorage, especially on a site with third-party scripts: any script on the page will read them.
  • The Refresh token changes with each update. Save the new one and don't use the old one: reuse VISH counts as theft and revokes all input.

Your website

  • CSRF. If your site uses cookies, protect changing requests with SameSite and a CSRF token (or verification of Origin). Addresses VISH are independent of this: VISH does not use cookies, the key and token are transmitted only by headers.
  • Open redirects. A parameter like ?returnTo= should be checked against the list of your addresses after logging in; do not redirect to anything from the address.
  • Clickjacking. Prohibit showing your login and confirmation pages in the frame: Content-Security-Policy: frame-ancestors 'none' (or X-Frame-Options: DENY). The consent page VISH itself is so protected and does not open in the frame.
  • XSS. Texts from VISH (notes, titles) are data: escape them when outputting, and display Markdown notes through the sanitizer.

Personal information

A person's name, group, role, and schedule are personal data. Take only what you need, keep it no longer than necessary, and delete it when a person has disabled your app (after that, GET /oauth/userinfo responds with its tokens with invalid_token). Explain to people in your privacy policy what you get from VISH and why.

Practical verification

The example creates state and nonce, compares them in constant time, checks address returnTo from the list and makes sure that VISH rejects the code in the request address and does not allow the consent page to be shown in the frame.:

// State and nonce, constant-time comparison, a returnTo allowlist, and two defences VISH enforces.
import { randomBytes, timingSafeEqual } from 'node:crypto'

const API = process.env.VISH_API ?? 'https://d1.vish-app.ru/api/v1'

const state = randomBytes(32).toString('base64url')
const nonce = randomBytes(32).toString('base64url')
const same = (left, right) =>
	typeof left === 'string' && left.length === right.length && timingSafeEqual(Buffer.from(left), Buffer.from(right))
if (!same(state, state) || same('forged', state) || state === nonce) throw new Error('state check is wrong')

// Only our own pages after sign-in: no open redirect through ?returnTo=.
const ORIGIN = 'https://app.example.org'
// A backslash reads as a slash to browsers, and a result starting with // is another host.
const safeReturnTo = (value) => {
	if (typeof value !== 'string' || value.includes('\\')) return '/'
	const target = new URL(value, ORIGIN)
	const path = target.pathname + target.search
	return target.origin === ORIGIN && path.startsWith('/') && !path.startsWith('//') ? path : '/'
}
const hostile = ['https://evil.example/', '//evil.example', '/.//evil.example', '/\\evil.example', '/\\\\evil.example', '\\\\evil.example']
if (safeReturnTo('/timetable?week=8') !== '/timetable?week=8' || hostile.some((value) => safeReturnTo(value) !== '/')) {
	throw new Error('returnTo check is wrong')
}
// Percent-encoded slashes stay a path of our own site.
if (safeReturnTo('%2F%2Fevil.example') !== '/%2F%2Fevil.example') throw new Error('returnTo check is wrong')

// VISH refuses a code or token in the URL of a protocol request.
const inUrl = await fetch(`${API}/oauth/token?code=leaked`, {
	method: 'POST',
	headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
	body: new URLSearchParams({ grant_type: 'authorization_code' }),
})
const refusal = await inUrl.json()
if (inUrl.status !== 400 || refusal.error !== 'invalid_request') throw new Error(`code in URL: ${inUrl.status} ${JSON.stringify(refusal)}`)

// VISH's consent page cannot be framed.
const consent = await fetch(new URL('/oauth/authorize', API))
const policy = `${consent.headers.get('content-security-policy')} ${consent.headers.get('x-frame-options')}`
if (!/frame-ancestors 'none'|DENY/u.test(policy)) throw new Error('consent page may be framed')

console.log('state/nonce, returnTo and constant-time checks pass')
console.log('code in URL ->', inUrl.status, refusal.error)
console.log('consent page framing ->', consent.headers.get('x-frame-options'))

We found a vulnerability

Write to us — the contact is listed at /.well-known/security.txt on the website VISH. Do not check the find on other people's accounts and production data: there is a stand d1 for experiments.

Verified: 2026-10, PWA aa337c45, platform bff65fce._