Merge pull request from GHSA-7pxq-6xx9-xpgm

* fix: fix improper authorization when accessing with third-party application

* refactor: refactor type definitions

* fix: get rid of unnecessary access limitation

* enhance: サードパーティアプリケーションがWebsocket APIを使えるように

* fix: add missing parentheses

* Revert "fix(backend): add missing kind definition for admin endpoints to improve security"

This reverts commit 5150053275.

* frontend: 翻訳の抜けを訂正, read:adminとwrite:adminはアクセス発行トークンのデフォルトでは非表示にする

* enhance(test): misskey-ghsa-7pxq-6xx9-xpgmに関するテストを追加

* enhance(test): Websocket APIに対するテストも追加

* enhance(refactor): `@/misc/api-permissions.ts`を`misskey-js/permissions`に統合

* fix(frontend): アクセストークン発行UIで全ての権限を有効にした際、管理者用APIへのアクセスも許可してしまう問題を修正

* enhance(backend): Websocketの接続に最低限必要な権限を変更

* fix(backend): `/api/admin/meta`をサードパーティアプリケーションからはアクセスできないように

* fix(backend): エンドポイントにアクセスするために必要な権限を変更

* fix(frontend/locale): Add missing type declaration

* chore: update `misskey-js/src/autogen`

---------

Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
Chocolate Pie 2023-12-27 15:08:59 +09:00 committed by GitHub
parent d87fecda7f
commit c96bc36fed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
148 changed files with 797 additions and 581 deletions

View file

@ -7,7 +7,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { IncomingMessage } from 'http';
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch } from '../utils.js';
import { signup, api, startServer, successfulApiCall, failedApiCall, uploadFile, waitFire, connectStream, relativeFetch, createAppToken } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
@ -89,6 +89,11 @@ describe('API', () => {
});
test('管理者専用のAPIのアクセス制限', async () => {
const application = await createAppToken(alice, ['read:account']);
const application2 = await createAppToken(alice, ['read:admin:index-stats']);
const application3 = await createAppToken(bob, []);
const application4 = await createAppToken(bob, ['read:admin:index-stats']);
// aliceは管理者、APIを使える
await successfulApiCall({
endpoint: '/admin/get-index-stats',
@ -128,6 +133,42 @@ describe('API', () => {
code: 'AUTHENTICATION_FAILED',
id: 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14',
});
await successfulApiCall({
endpoint: '/admin/get-index-stats',
parameters: {},
user: { token: application2 },
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
parameters: {},
user: { token: application },
}, {
status: 403,
code: 'PERMISSION_DENIED',
id: '1370e5b7-d4eb-4566-bb1d-7748ee6a1838',
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
parameters: {},
user: { token: application3 },
}, {
status: 403,
code: 'ROLE_PERMISSION_DENIED',
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
});
await failedApiCall({
endpoint: '/admin/get-index-stats',
parameters: {},
user: { token: application4 },
}, {
status: 403,
code: 'ROLE_PERMISSION_DENIED',
id: 'c3d38592-54c0-429d-be96-5636b0431a61',
});
});
describe('Authentication header', () => {

View file

@ -6,8 +6,9 @@
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { WebSocket } from 'ws';
import { MiFollowing } from '@/models/Following.js';
import { signup, api, post, startServer, initTestDb, waitFire } from '../utils.js';
import { signup, api, post, startServer, initTestDb, waitFire, createAppToken, port } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
@ -560,6 +561,28 @@ describe('Streaming', () => {
});
});
test('Authentication', async () => {
const application = await createAppToken(ayano, []);
const application2 = await createAppToken(ayano, ['read:account']);
const socket = new WebSocket(`ws://127.0.0.1:${port}/streaming?i=${application}`);
const established = await new Promise<boolean>((resolve, reject) => {
socket.on('error', () => resolve(false));
socket.on('unexpected-response', () => resolve(false));
setTimeout(() => resolve(true), 3000);
});
socket.close();
assert.strictEqual(established, false);
const fired = await waitFire(
{ token: application2 }, 'hybridTimeline',
() => api('notes/create', { text: 'Hello, world!' }, ayano),
msg => msg.type === 'note' && msg.body.userId === ayano.id,
);
assert.strictEqual(fired, true);
});
// XXX: QueryFailedError: duplicate key value violates unique constraint "IDX_347fec870eafea7b26c8a73bac"
/*
describe('Hashtag Timeline', () => {

View file

@ -6,6 +6,7 @@
import * as assert from 'node:assert';
import { readFile } from 'node:fs/promises';
import { isAbsolute, basename } from 'node:path';
import { randomUUID } from 'node:crypto';
import { inspect } from 'node:util';
import WebSocket, { ClientOptions } from 'ws';
import fetch, { File, RequestInit } from 'node-fetch';
@ -126,6 +127,15 @@ export const post = async (user: UserToken, params?: misskey.Endpoints['notes/cr
return res.body ? res.body.createdNote : null;
};
export const createAppToken = async (user: UserToken, permissions: (typeof misskey.permissions)[number][]) => {
const res = await api('miauth/gen-token', {
session: randomUUID(),
permission: permissions,
}, user);
return (res.body as misskey.entities.MiauthGenTokenResponse).token;
};
// 非公開ートをAPI越しに見たときのート NoteEntityService.ts
export const hiddenNote = (note: any): any => {
const temp = {