mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-25 17:10:43 +00:00
Merge branch 'develop' into room
This commit is contained in:
commit
985dd72a7f
13 changed files with 229 additions and 57 deletions
|
|
@ -10,6 +10,15 @@
|
|||
-
|
||||
|
||||
|
||||
## 2026.5.4
|
||||
|
||||
### General
|
||||
- セキュリティに関する修正
|
||||
|
||||
### Client
|
||||
- Fix: ビルドに失敗することがある問題を修正
|
||||
|
||||
|
||||
## 2026.5.3
|
||||
|
||||
### General
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.5.3",
|
||||
"version": "2026.5.4",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@
|
|||
"ulid": "3.0.2",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.7",
|
||||
"ws": "8.20.0",
|
||||
"ws": "8.20.1",
|
||||
"xev": "3.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -182,11 +182,12 @@ export class AnnouncementService {
|
|||
@bindThis
|
||||
public async getAnnouncement(announcementId: MiAnnouncement['id'], me: MiUser | null): Promise<Packed<'Announcement'>> {
|
||||
const announcement = await this.announcementsRepository.findOneByOrFail({ id: announcementId });
|
||||
if (me) {
|
||||
if (announcement.userId && announcement.userId !== me.id) {
|
||||
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
|
||||
}
|
||||
|
||||
if (announcement.userId && (me == null || announcement.userId !== me.id)) {
|
||||
throw new EntityNotFoundError(this.announcementsRepository.metadata.target, { id: announcementId });
|
||||
}
|
||||
|
||||
if (me) {
|
||||
const read = await this.announcementReadsRepository.findOneBy({
|
||||
announcementId: announcement.id,
|
||||
userId: me.id,
|
||||
|
|
|
|||
|
|
@ -572,6 +572,27 @@ export class ChatService {
|
|||
return created;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async hasPermissionToViewRoomInfo(meId: MiUser['id'], room: MiChatRoom) {
|
||||
if (room.ownerId === meId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await this.isRoomMember(room, meId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await this.chatRoomInvitationsRepository.findOneBy({ roomId: room.id, userId: meId })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (await this.roleService.isModerator({ id: meId })) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async hasPermissionToDeleteRoom(meId: MiUser['id'], room: MiChatRoom) {
|
||||
if (room.ownerId === meId) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { RsaKeyPair } from 'slacc';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { CONTEXT, PRELOADED_CONTEXTS } from './misc/contexts.js';
|
||||
import { validateContentTypeSetAsJsonLD } from './misc/validator.js';
|
||||
import type { JsonLdDocument } from 'jsonld';
|
||||
|
|
@ -16,7 +17,40 @@ import type { JsonLd as JsonLdObject, RemoteDocument } from 'jsonld/jsonld-spec.
|
|||
|
||||
// RsaSignature2017 implementation is based on https://github.com/transmute-industries/RsaSignature2017
|
||||
|
||||
class JsonLd {
|
||||
export class JsonLdError extends IdentifiableError {
|
||||
constructor(id: string, message?: string) {
|
||||
super(id, message);
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonLdCacheOverflowError extends JsonLdError {
|
||||
constructor() {
|
||||
super('42fb039c-69fb-4f75-8187-d3aee412423e', 'context cache overflow');
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonLdCacheFrozenError extends JsonLdError {
|
||||
constructor() {
|
||||
super('202c41fa-72d5-4e22-95af-94a8ac83346f', 'attempt to insert into frozen context cache');
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonLdForbiddenDirectiveError extends JsonLdError {
|
||||
constructor(public directive: string) {
|
||||
super('0297f79b-0ed9-4b6c-875f-b0a82ff96781', `${directive} is forbidden by Misskey in ActivityPub documents`);
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonLd {
|
||||
private static forbiddenDirectives = new Set([
|
||||
'@included',
|
||||
'@graph',
|
||||
'@reverse',
|
||||
]);
|
||||
|
||||
private frozen = false;
|
||||
private cache: Map<string, RemoteDocument> = new Map();
|
||||
|
||||
public debug = false;
|
||||
public preLoad = true;
|
||||
public loderTimeout = 5000;
|
||||
|
|
@ -81,9 +115,9 @@ class JsonLd {
|
|||
const optionsHash = this.sha256(canonizedOptions.toString());
|
||||
const transformedData = { ...data };
|
||||
delete transformedData['signature'];
|
||||
const cannonidedData = await this.normalize(transformedData);
|
||||
if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`);
|
||||
const documentHash = this.sha256(cannonidedData.toString());
|
||||
const cannonizedData = await this.normalize(transformedData);
|
||||
if (this.debug) console.debug(`cannonizedData: ${cannonizedData}`);
|
||||
const documentHash = this.sha256(cannonizedData.toString());
|
||||
const verifyData = `${optionsHash}${documentHash}`;
|
||||
return verifyData;
|
||||
}
|
||||
|
|
@ -106,6 +140,34 @@ class JsonLd {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent any further HTTP requests from being made for the sake of
|
||||
* validating JSON-LD signatures.
|
||||
*/
|
||||
@bindThis
|
||||
public freeze(): void { this.frozen = true; }
|
||||
|
||||
@bindThis
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public checkForForbiddenDirectives(value: any): void {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) this.checkForForbiddenDirectives(item);
|
||||
} else {
|
||||
const object = value;
|
||||
for (const [key, value] of Object.entries(object)) {
|
||||
if (JsonLd.forbiddenDirectives.has(key)) {
|
||||
throw new JsonLdForbiddenDirectiveError(key);
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
this.checkForForbiddenDirectives(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private getLoader() {
|
||||
return async (url: string): Promise<RemoteDocument> => {
|
||||
|
|
@ -122,13 +184,27 @@ class JsonLd {
|
|||
}
|
||||
}
|
||||
|
||||
const cached = this.cache.get(url);
|
||||
if (cached) {
|
||||
if (this.debug) console.debug(`HIT: ${url}`);
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (this.debug) console.debug(`MISS: ${url}`);
|
||||
|
||||
if (this.frozen) throw new JsonLdCacheFrozenError();
|
||||
|
||||
const document = await this.fetchDocument(url);
|
||||
return {
|
||||
this.checkForForbiddenDirectives(document);
|
||||
|
||||
const remoteDocument = {
|
||||
contextUrl: undefined,
|
||||
document: document,
|
||||
documentUrl: url,
|
||||
};
|
||||
this.cache.set(url, remoteDocument);
|
||||
if (this.cache.size > 256) throw new JsonLdCacheOverflowError();
|
||||
return remoteDocument;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
|
|||
import { StatusError } from '@/misc/status-error.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
|
||||
import { JsonLdError, JsonLdService } from '@/core/activitypub/JsonLdService.js';
|
||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
|
@ -163,22 +163,17 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
|||
|
||||
const jsonLd = this.jsonLdService.use();
|
||||
|
||||
// LD-Signature検証
|
||||
const verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem).catch(() => false);
|
||||
if (!verified) {
|
||||
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
||||
}
|
||||
|
||||
// アクティビティを正規化
|
||||
delete activity.signature;
|
||||
try {
|
||||
activity = await jsonLd.compact(activity) as IActivity;
|
||||
} catch (e) {
|
||||
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${e}`);
|
||||
} catch (error) {
|
||||
throw new Bull.UnrecoverableError(`skip: failed to compact activity: ${error}`);
|
||||
}
|
||||
try {
|
||||
jsonLd.checkForForbiddenDirectives(activity);
|
||||
} catch (error) {
|
||||
throw new Bull.UnrecoverableError(`skip: ${error}`);
|
||||
}
|
||||
// TODO: 元のアクティビティと非互換な形に正規化される場合は転送をスキップする
|
||||
// https://github.com/mastodon/mastodon/blob/664b0ca/app/services/activitypub/process_collection_service.rb#L24-L29
|
||||
activity.signature = ldSignature;
|
||||
|
||||
//#region Log
|
||||
const compactedInfo = Object.assign({}, activity);
|
||||
|
|
@ -186,6 +181,25 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
|||
this.logger.debug(`compacted: ${JSON.stringify(compactedInfo, null, 2)}`);
|
||||
//#endregion
|
||||
|
||||
activity.signature = ldSignature;
|
||||
|
||||
jsonLd.freeze();
|
||||
|
||||
// LD-Signature検証
|
||||
let verified;
|
||||
try {
|
||||
verified = await jsonLd.verifyRsaSignature2017(activity, authUser.key.keyPem);
|
||||
if (!verified) {
|
||||
throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof JsonLdError) {
|
||||
throw new Bull.UnrecoverableError(`skip: encountered a JSON-LD error while verifying signature: ${error}`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// もう一度actorチェック
|
||||
if (authUser.user.uri !== getApId(activity.actor)) {
|
||||
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${getApId(activity.actor)})`);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
if (!await this.chatService.hasPermissionToViewRoomInfo(me.id, room)) {
|
||||
throw new ApiError(meta.errors.noSuchRoom);
|
||||
}
|
||||
|
||||
return this.chatEntityService.packRoom(room, me);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export type Theme = {
|
|||
|
||||
export type CompiledTheme = Record<string, string>;
|
||||
|
||||
const MAX_THEME_REFERENCE_DEPTH = 8;
|
||||
|
||||
export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X'));
|
||||
|
||||
export const getBuiltinThemes = () => Promise.all(
|
||||
|
|
@ -56,45 +58,68 @@ export const getBuiltinThemes = () => Promise.all(
|
|||
].map(name => import(`@@/themes/${name}.json5`).then(({ default: _default }): Theme => _default)),
|
||||
);
|
||||
|
||||
export function compile(theme: Theme): CompiledTheme {
|
||||
function getColor(val: string): tinycolor.Instance {
|
||||
if (val[0] === '@') { // ref (prop)
|
||||
return getColor(theme.props[val.substring(1)]);
|
||||
} else if (val[0] === '$') { // ref (const)
|
||||
return getColor(theme.props[val]);
|
||||
} else if (val[0] === ':') { // func
|
||||
const parts = val.split('<');
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(parts.join('<'));
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
function getThemeReferenceColor(theme: Theme, key: string, stack: string[], depth: number): tinycolor.Instance {
|
||||
if (depth >= MAX_THEME_REFERENCE_DEPTH) {
|
||||
throw new Error('Theme reference limit exceeded');
|
||||
}
|
||||
|
||||
if (stack.includes(key)) {
|
||||
throw new Error('Theme contains circular references');
|
||||
}
|
||||
|
||||
const nextValue = theme.props[key];
|
||||
if (typeof nextValue !== 'string') {
|
||||
throw new Error(`Theme references missing property: ${key}`);
|
||||
}
|
||||
|
||||
return getColor(theme, nextValue, [...stack, key], depth + 1);
|
||||
}
|
||||
|
||||
function getColor(theme: Theme, val: string, stack: string[] = [], depth = 0): tinycolor.Instance {
|
||||
if (val[0] === '@') { // ref (prop)
|
||||
return getThemeReferenceColor(theme, val.substring(1), stack, depth);
|
||||
} else if (val[0] === '$') { // ref (const)
|
||||
return getThemeReferenceColor(theme, val, stack, depth);
|
||||
} else if (val[0] === ':') { // func
|
||||
if (depth >= MAX_THEME_REFERENCE_DEPTH) {
|
||||
throw new Error('Theme reference limit exceeded');
|
||||
}
|
||||
|
||||
const parts = val.split('<');
|
||||
const funcTxt = parts.shift();
|
||||
const argTxt = parts.shift();
|
||||
|
||||
if (funcTxt && argTxt) {
|
||||
const func = funcTxt.substring(1);
|
||||
const arg = parseFloat(argTxt);
|
||||
const color = getColor(theme, parts.join('<'), stack, depth + 1);
|
||||
|
||||
switch (func) {
|
||||
case 'darken': return color.darken(arg);
|
||||
case 'lighten': return color.lighten(arg);
|
||||
case 'alpha': return color.setAlpha(arg);
|
||||
case 'hue': return color.spin(arg);
|
||||
case 'saturate': return color.saturate(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// other case
|
||||
return tinycolor(val);
|
||||
}
|
||||
|
||||
export function compile(theme: Theme): CompiledTheme {
|
||||
const props = {} as CompiledTheme;
|
||||
|
||||
for (const [k, v] of Object.entries(theme.props)) {
|
||||
if (k.startsWith('$')) continue; // ignore const
|
||||
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
|
||||
props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(theme, v));
|
||||
}
|
||||
|
||||
return props;
|
||||
return Object.fromEntries(
|
||||
Object.entries(props).filter(([key]) => themeProps.includes(key)),
|
||||
) as CompiledTheme;
|
||||
}
|
||||
|
||||
function genValue(c: tinycolor.Instance): string {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2026.5.3",
|
||||
"version": "2026.5.4",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
|
|||
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
|
|
@ -391,8 +391,8 @@ importers:
|
|||
specifier: 3.6.7
|
||||
version: 3.6.7
|
||||
ws:
|
||||
specifier: 8.20.0
|
||||
version: 8.20.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)
|
||||
specifier: 8.20.1
|
||||
version: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)
|
||||
xev:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
|
|
@ -5485,7 +5485,7 @@ packages:
|
|||
engines: {node: '>= 14'}
|
||||
|
||||
aiscript-vscode@https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67}
|
||||
resolution: {gitHosted: true, integrity: sha512-S4eSTHasZz29AMlnU2/zdGP8zikiDiYfYW9kNooAfwVo8OghXdxKuTDDKDAjWsbBxa1+P4uQHa4BNk9MY74rJQ==, tarball: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/1dc7f60cda78d030dadfc518a33c472202b2ef67}
|
||||
version: 0.1.16
|
||||
engines: {vscode: ^1.83.0}
|
||||
|
||||
|
|
@ -9999,7 +9999,7 @@ packages:
|
|||
engines: {node: '>= 0.4'}
|
||||
|
||||
storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640:
|
||||
resolution: {gitHosted: true, tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
|
||||
resolution: {gitHosted: true, integrity: sha512-QaH1uZSlApQ2CZPkHfhmNm89I92L02s3MdbUPG66TmAyqMaqzxd/AvobORBjtTZ0ymUSa3ii482dRXi+fFb19w==, tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
|
||||
version: 0.0.0
|
||||
peerDependencies:
|
||||
'@storybook/blocks': ^7.0.0-rc.4
|
||||
|
|
@ -11039,6 +11039,18 @@ packages:
|
|||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
ws@8.20.1:
|
||||
resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
|
||||
wsl-utils@0.1.0:
|
||||
resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -22492,6 +22504,11 @@ snapshots:
|
|||
bufferutil: 4.1.0
|
||||
utf-8-validate: 6.0.6
|
||||
|
||||
ws@8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6):
|
||||
optionalDependencies:
|
||||
bufferutil: 4.1.0
|
||||
utf-8-validate: 6.0.6
|
||||
|
||||
wsl-utils@0.1.0:
|
||||
dependencies:
|
||||
is-wsl: 3.1.1
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ minimumReleaseAgeExclude:
|
|||
- systeminformation # 脆弱性対応。そのうち消す
|
||||
- sanitize-html # 脆弱性対応。そのうち消す
|
||||
- launder # 脆弱性対応。そのうち消す
|
||||
# Renovate security update: ws@8.20.1
|
||||
- ws@8.20.1
|
||||
overrides:
|
||||
'@aiscript-dev/aiscript-languageserver': '-'
|
||||
chokidar: 5.0.0
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
extends: [
|
||||
'config:recommended',
|
||||
],
|
||||
toolSettings: {
|
||||
nodeMaxMemory: 4096,
|
||||
},
|
||||
timezone: 'Asia/Tokyo',
|
||||
schedule: [
|
||||
'* 0 * * *',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue