Merge pull request #17458 from misskey-dev/develop

Release: 2026.5.4
This commit is contained in:
misskey-release-bot[bot] 2026-05-21 00:32:02 +00:00 committed by GitHub
commit 3ac6d287d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 235 additions and 59 deletions

View file

@ -1,3 +1,15 @@
## 2026.5.4
### General
- セキュリティに関する修正
### Client
- Fix: ビルドに失敗することがある問題を修正
### Server
-
## 2026.5.3
### General

View file

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2026.5.3",
"version": "2026.5.4",
"codename": "nasubi",
"repository": {
"type": "git",

View file

@ -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": {

View file

@ -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,

View file

@ -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) {

View file

@ -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;
};
}

View file

@ -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)})`);

View file

@ -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);
});
}

View file

@ -58,7 +58,7 @@ export class LocaleInliner {
collectsModifications() {
for (const chunk of this.chunks) {
if (!chunk.sourceCode) {
if (chunk.sourceCode == null) {
throw new Error(`Source code for ${chunk.fileName} is not loaded.`);
}
const fileLogger = this.logger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);
@ -80,7 +80,7 @@ export class LocaleInliner {
await fs.mkdir(path.join(this.outputDir, localeName), { recursive: true });
const localeLogger = localeName === 'ja-JP' ? this.logger : blankLogger; // we want to log for single locale only
for (const chunk of this.chunks) {
if (!chunk.sourceCode || !chunk.modifications) {
if (chunk.sourceCode == null || !chunk.modifications) {
throw new Error(`Source code or modifications for ${chunk.fileName} is not available.`);
}
const fileLogger = localeLogger.prefixed(`${chunk.fileName} (${chunk.chunkName}): `);

View file

@ -18,6 +18,7 @@ interface WalkerContext {
}
export function collectModifications(sourceCode: string, fileName: string, fileLogger: Logger, inliner: LocaleInliner): TextModification[] {
if (sourceCode === '') return [];
let programNode: RolldownESTree.Program;
try {
programNode = parseAst(sourceCode);

View file

@ -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 {

View file

@ -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
View file

@ -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
@ -4839,7 +4839,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}
@ -9325,7 +9325,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
@ -10353,6 +10353,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'}
@ -20504,6 +20516,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

View file

@ -58,6 +58,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

View file

@ -3,6 +3,9 @@
extends: [
'config:recommended',
],
toolSettings: {
nodeMaxMemory: 4096,
},
timezone: 'Asia/Tokyo',
schedule: [
'* 0 * * *',