VISH 应用——俄罗斯交通大学课表

面向所有学院学生和教职工的俄罗斯交通大学课表。按天和按周查看自己班级和任意教师的课表、此刻的空闲教室、教室、班级和教师的使用情况、课表变更通知以及课表人工智能助手。

学院

俄罗斯交通大学应用涵盖五个学院:高等工程学院、民航学院、道路管理学院、先进工程学院“高铁学院”和国际交通运输学院。在任何学院,班级课表和教师课表的查找方式都相同。

VISH 应用的功能
栏目
指南

集成安全

更新于 27.10.2026

在你身边做什么,以便与VISH不会成为一个洞:它包含密钥,令牌和学生数据。 该列表将根据10月份的工作方式进行检查VISH,2026,并与OAuth(RFC9700)的建议。

API密钥

  • 密钥仅在服务器上。 不要将其插入到页面代码,移动应用程序或浏览器扩展中:任何人都可以从那里获得它。 如果程序是为用户运行的,它不需要你的密钥,而是一个“入口处VISH”.
  • 保密,不在存储库中:环境变量,秘密存储库(Lockbox,Vault,CI secrets)。 关键从vish_. 使用这个前缀,通过搜索泄漏很容易找到它。
  • 最低权利。 发布时,只标记程序需要的内容。 您根本不需要密钥来读取公共数据。
  • 任期。 设置一个合理的结束日期;需要一个自签发的密钥(不超过730天)。
  • 如果它亮起,请用重新-释放按钮“开发人员”页:旧行立即停止工作。
  • Не пишите в журналы заголовки X-Api-Key и Authorization, адреса с параметрами и тела ответов с личными данными.

“透过VISH”

  • PKCE S256 总是可用的,也适用于封闭的应用程序。VISH其他选择,不接受。
  • state -随机,你自己的每个输入;在任何其他操作之前在返回地址中检查它。 这是针对登录伪造(登录CSRF)的保护。
  • nonce 是随机的,从nonce到id_token:这样ID令牌不能从别人的输入中滑落。
  • iss в адресе возврата должен совпадать с issuer из документа настройки — защита от путаницы между провайдерами (mix-up).
  • 确切的回邮地址。 准确登记您要返回的地址; VISH逐字符比较它们,并且不允许*. 请勿将退货地址设为“通用重定向器”.
  • 支票id_token: iss, aud -你的client_id, nonce, exp. 签名(ES256)-使用密钥出jwks_uri,如果令牌没有直接来自/oauth/token回应。
  • Минимум разрешений. Просите только нужные scope: человеку показывают весь список, и длинный список — повод отказать.
  • 出路是回顾。 当用户注销您的应用程序时,请在之后撤消刷新令牌POST /oauth/revoke.

在哪里存储令牌

  • 令牌不在地址中。 都不是access_token也没有refresh_token应包含在页面地址中, Referer或浏览器历史记录。VISH它只在响应正文中发出它们,并拒绝协议请求地址中的代码和令牌。
  • 最重要的是,在服务器上 (backend-for-frontend scheme):浏览器只为您的站点保存带有标志的会话cookie HttpOnly, Secure, SameSite=Lax,和令牌VISH都存储在服务器上。
  • 如果只是浏览器,将令牌保留在页面的内存中,并且code_verifier和state在sessionStorage为登录时间。 不要放入令牌localStorage,特别是在具有第三方脚本的网站上:页面上的任何脚本都会读取它们。
  • 刷新令牌随每次更新而变化。 保存新的,不要使用旧的:重用VISH算作盗窃并撤销所有输入。

你的网站

  • CSRF。 如果您的网站使用cookie,请使用SameSite和CSRF令牌(或验证Origin). 地址VISH独立于此: VISH不使用cookie,密钥和令牌仅通过标头传输。
  • 打开重定向。 像这样的参数?returnTo=登录后应根据您的地址列表进行检查;不要重定向到地址中的任何内容。
  • 点击劫持。 禁止在框架中显示您的登录和确认页面: Content-Security-Policy: frame-ancestors 'none'(或X-Frame-Options: DENY). 同意书页VISH本身是如此保护,并没有在框架中打开。
  • XSS。 来自VISH(notes,title)是数据:在输出时转义它们,并通过消毒器显示Markdown笔记。

个人资料

一个人的姓名、组、角色和日程安排是个人数据。 只取你需要的东西,不要超过必要的时间,并在有人禁用你的应用程序时删除它(之后), GET /oauth/userinfo用它的令牌回应invalid_token). 在您的隐私政策中向人们解释您从中获得的信息VISH为什么。

实际验证

示例创建state和nonce,在恒定时间内比较它们,检查地址returnTo从列表中,并确保VISH拒绝请求地址中的代码,并且不允许在框架中显示同意页面。:

// 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'))

我们发现了一个漏洞

写信给我们-联络人列于/.well-known/security.txt在网站上VISH. 不要检查其他人的帐户和生产数据上的发现:有一个试验台d1。

验证:2026-10,PWA aa337c45,平台bff65fce._