集成安全
更新于 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._