VYPR
High severityOSV Advisory· Published Dec 15, 2025· Updated Dec 16, 2025

misskey.js's export data contains private post data

CVE-2025-66402

Description

Misskey is an open source, federated social media platform. Starting in version 13.0.0-beta.16 and prior to version 2025.12.0, an actor who does not have permission to view favorites or clips can can export the posts and view the contents. Version 2025.12.0 fixes the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
misskey-jsnpm
>= 13.0.0-beta.16, < 2025.12.02025.12.0

Affected products

1

Patches

1
dc77d59f8712

Merge commit from fork

https://github.com/misskey-dev/misskeyおさむのひとDec 6, 2025via ghsa
5 files changed · +221 52
  • packages/backend/src/core/entities/NoteEntityService.ts+3 12 modified
    @@ -15,6 +15,7 @@ import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepos
     import { bindThis } from '@/decorators.js';
     import { DebounceLoader } from '@/misc/loader.js';
     import { IdService } from '@/core/IdService.js';
    +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
     import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
     import type { OnModuleInit } from '@nestjs/common';
     import type { CustomEmojiService } from '../CustomEmojiService.js';
    @@ -116,12 +117,7 @@ export class NoteEntityService implements OnModuleInit {
     	private treatVisibility(packedNote: Packed<'Note'>): Packed<'Note'>['visibility'] {
     		if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
     			const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
    -			if ((followersOnlyBefore != null)
    -				&& (
    -					(followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
    -					|| (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
    -				)
    -			) {
    +			if (shouldHideNoteByTime(followersOnlyBefore, packedNote.createdAt)) {
     				packedNote.visibility = 'followers';
     			}
     		}
    @@ -141,12 +137,7 @@ export class NoteEntityService implements OnModuleInit {
     
     		if (!hide) {
     			const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
    -			if ((hiddenBefore != null)
    -				&& (
    -					(hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
    -					|| (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
    -				)
    -			) {
    +			if (shouldHideNoteByTime(hiddenBefore, packedNote.createdAt)) {
     				hide = true;
     			}
     		}
    
  • packages/backend/src/misc/should-hide-note-by-time.ts+29 0 added
    @@ -0,0 +1,29 @@
    +/*
    + * SPDX-FileCopyrightText: syuilo and misskey-project
    + * SPDX-License-Identifier: AGPL-3.0-only
    + */
    +
    +/**
    + * ノートが指定された時間条件に基づいて非表示対象かどうかを判定する
    + * @param hiddenBefore 非表示条件(負の値: 作成からの経過秒数、正の値: UNIXタイムスタンプ秒、null: 判定しない)
    + * @param createdAt ノートの作成日時(ISO 8601形式の文字列 または Date オブジェクト)
    + * @returns 非表示にすべき場合は true
    + */
    +export function shouldHideNoteByTime(hiddenBefore: number | null | undefined, createdAt: string | Date): boolean {
    +	if (hiddenBefore == null) {
    +		return false;
    +	}
    +
    +	const createdAtTime = typeof createdAt === 'string' ? new Date(createdAt).getTime() : createdAt.getTime();
    +
    +	if (hiddenBefore <= 0) {
    +		// 負の値: 作成からの経過時間(秒)で判定
    +		const elapsedSeconds = (Date.now() - createdAtTime) / 1000;
    +		const hideAfterSeconds = Math.abs(hiddenBefore);
    +		return elapsedSeconds > hideAfterSeconds;
    +	} else {
    +		// 正の値: 絶対的なタイムスタンプ(秒)で判定
    +		const createdAtSeconds = createdAtTime / 1000;
    +		return createdAtSeconds < hiddenBefore;
    +	}
    +}
    
  • packages/backend/src/queue/processors/ExportClipsProcessorService.ts+36 28 modified
    @@ -5,21 +5,20 @@
     
     import * as fs from 'node:fs';
     import { Writable } from 'node:stream';
    -import { Inject, Injectable, StreamableFile } from '@nestjs/common';
    -import { MoreThan } from 'typeorm';
    +import { Inject, Injectable } from '@nestjs/common';
     import { format as dateFormat } from 'date-fns';
     import { DI } from '@/di-symbols.js';
    -import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, NotesRepository, PollsRepository, UsersRepository } from '@/models/_.js';
    +import type { ClipNotesRepository, ClipsRepository, MiClip, MiClipNote, MiUser, PollsRepository, UsersRepository } from '@/models/_.js';
     import type Logger from '@/logger.js';
     import { DriveService } from '@/core/DriveService.js';
     import { createTemp } from '@/misc/create-temp.js';
     import type { MiPoll } from '@/models/Poll.js';
     import type { MiNote } from '@/models/Note.js';
     import { bindThis } from '@/decorators.js';
    -import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
    -import { Packed } from '@/misc/json-schema.js';
     import { IdService } from '@/core/IdService.js';
     import { NotificationService } from '@/core/NotificationService.js';
    +import { QueryService } from '@/core/QueryService.js';
    +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
     import { QueueLoggerService } from '../QueueLoggerService.js';
     import type * as Bull from 'bullmq';
     import type { DbJobDataWithUser } from '../types.js';
    @@ -43,6 +42,7 @@ export class ExportClipsProcessorService {
     
     		private driveService: DriveService,
     		private queueLoggerService: QueueLoggerService,
    +		private queryService: QueryService,
     		private idService: IdService,
     		private notificationService: NotificationService,
     	) {
    @@ -100,16 +100,16 @@ export class ExportClipsProcessorService {
     		});
     
     		while (true) {
    -			const clips = await this.clipsRepository.find({
    -				where: {
    -					userId: user.id,
    -					...(cursor ? { id: MoreThan(cursor) } : {}),
    -				},
    -				take: 100,
    -				order: {
    -					id: 1,
    -				},
    -			});
    +			const query = this.clipsRepository.createQueryBuilder('clip')
    +				.where('clip.userId = :userId', { userId: user.id })
    +				.orderBy('clip.id', 'ASC')
    +				.take(100);
    +
    +			if (cursor) {
    +				query.andWhere('clip.id > :cursor', { cursor });
    +			}
    +
    +			const clips = await query.getMany();
     
     			if (clips.length === 0) {
     				job.updateProgress(100);
    @@ -124,7 +124,7 @@ export class ExportClipsProcessorService {
     				const isFirst = exportedClipsCount === 0;
     				await writer.write(isFirst ? content : ',\n' + content);
     
    -				await this.processClipNotes(writer, clip.id);
    +				await this.processClipNotes(writer, clip.id, user.id);
     
     				await writer.write(']}');
     				exportedClipsCount++;
    @@ -134,22 +134,25 @@ export class ExportClipsProcessorService {
     		}
     	}
     
    -	async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string): Promise<void> {
    +	async processClipNotes(writer: WritableStreamDefaultWriter, clipId: string, userId: string): Promise<void> {
     		let exportedClipNotesCount = 0;
     		let cursor: MiClipNote['id'] | null = null;
     
     		while (true) {
    -			const clipNotes = await this.clipNotesRepository.find({
    -				where: {
    -					clipId,
    -					...(cursor ? { id: MoreThan(cursor) } : {}),
    -				},
    -				take: 100,
    -				order: {
    -					id: 1,
    -				},
    -				relations: ['note', 'note.user'],
    -			}) as (MiClipNote & { note: MiNote & { user: MiUser } })[];
    +			const query = this.clipNotesRepository.createQueryBuilder('clipNote')
    +				.leftJoinAndSelect('clipNote.note', 'note')
    +				.leftJoinAndSelect('note.user', 'user')
    +				.where('clipNote.clipId = :clipId', { clipId })
    +				.orderBy('clipNote.id', 'ASC')
    +				.take(100);
    +
    +			if (cursor) {
    +				query.andWhere('clipNote.id > :cursor', { cursor });
    +			}
    +
    +			this.queryService.generateVisibilityQuery(query, { id: userId });
    +
    +			const clipNotes = await query.getMany() as (MiClipNote & { note: MiNote & { user: MiUser } })[];
     
     			if (clipNotes.length === 0) {
     				break;
    @@ -158,6 +161,11 @@ export class ExportClipsProcessorService {
     			cursor = clipNotes.at(-1)?.id ?? null;
     
     			for (const clipNote of clipNotes) {
    +				const noteCreatedAt = this.idService.parse(clipNote.note.id).date;
    +				if (shouldHideNoteByTime(clipNote.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
    +					continue;
    +				}
    +
     				let poll: MiPoll | undefined;
     				if (clipNote.note.hasPoll) {
     					poll = await this.pollsRepository.findOneByOrFail({ noteId: clipNote.note.id });
    
  • packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts+22 12 modified
    @@ -5,7 +5,6 @@
     
     import * as fs from 'node:fs';
     import { Inject, Injectable } from '@nestjs/common';
    -import { MoreThan } from 'typeorm';
     import { format as dateFormat } from 'date-fns';
     import { DI } from '@/di-symbols.js';
     import type { MiNoteFavorite, NoteFavoritesRepository, PollsRepository, MiUser, UsersRepository } from '@/models/_.js';
    @@ -17,6 +16,8 @@ import type { MiNote } from '@/models/Note.js';
     import { bindThis } from '@/decorators.js';
     import { IdService } from '@/core/IdService.js';
     import { NotificationService } from '@/core/NotificationService.js';
    +import { QueryService } from '@/core/QueryService.js';
    +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
     import { QueueLoggerService } from '../QueueLoggerService.js';
     import type * as Bull from 'bullmq';
     import type { DbJobDataWithUser } from '../types.js';
    @@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
     
     		private driveService: DriveService,
     		private queueLoggerService: QueueLoggerService,
    +		private queryService: QueryService,
     		private idService: IdService,
     		private notificationService: NotificationService,
     	) {
    @@ -83,17 +85,20 @@ export class ExportFavoritesProcessorService {
     			});
     
     			while (true) {
    -				const favorites = await this.noteFavoritesRepository.find({
    -					where: {
    -						userId: user.id,
    -						...(cursor ? { id: MoreThan(cursor) } : {}),
    -					},
    -					take: 100,
    -					order: {
    -						id: 1,
    -					},
    -					relations: ['note', 'note.user'],
    -				}) as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
    +				const query = this.noteFavoritesRepository.createQueryBuilder('favorite')
    +					.leftJoinAndSelect('favorite.note', 'note')
    +					.leftJoinAndSelect('note.user', 'user')
    +					.where('favorite.userId = :userId', { userId: user.id })
    +					.orderBy('favorite.id', 'ASC')
    +					.take(100);
    +
    +				if (cursor) {
    +					query.andWhere('favorite.id > :cursor', { cursor });
    +				}
    +
    +				this.queryService.generateVisibilityQuery(query, { id: user.id });
    +
    +				const favorites = await query.getMany() as (MiNoteFavorite & { note: MiNote & { user: MiUser } })[];
     
     				if (favorites.length === 0) {
     					job.updateProgress(100);
    @@ -103,6 +108,11 @@ export class ExportFavoritesProcessorService {
     				cursor = favorites.at(-1)?.id ?? null;
     
     				for (const favorite of favorites) {
    +					const noteCreatedAt = this.idService.parse(favorite.note.id).date;
    +					if (shouldHideNoteByTime(favorite.note.user.makeNotesHiddenBefore, noteCreatedAt)) {
    +						continue;
    +					}
    +
     					let poll: MiPoll | undefined;
     					if (favorite.note.hasPoll) {
     						poll = await this.pollsRepository.findOneByOrFail({ noteId: favorite.note.id });
    
  • packages/backend/test/unit/misc/should-hide-note-by-time.ts+131 0 added
    @@ -0,0 +1,131 @@
    +/*
    + * SPDX-FileCopyrightText: syuilo and misskey-project
    + * SPDX-License-Identifier: AGPL-3.0-only
    + */
    +
    +import { describe, expect, test, beforeEach, afterEach, jest } from '@jest/globals';
    +import { shouldHideNoteByTime } from '@/misc/should-hide-note-by-time.js';
    +
    +describe('misc:should-hide-note-by-time', () => {
    +	let now: number;
    +
    +	beforeEach(() => {
    +		now = Date.now();
    +		jest.useFakeTimers();
    +		jest.setSystemTime(now);
    +	});
    +
    +	afterEach(() => {
    +		jest.useRealTimers();
    +	});
    +
    +	describe('hiddenBefore が null または undefined の場合', () => {
    +		test('hiddenBefore が null のときは false を返す(非表示機能が有効でない)', () => {
    +			const createdAt = new Date(now - 86400000); // 1 day ago
    +			expect(shouldHideNoteByTime(null, createdAt)).toBe(false);
    +		});
    +
    +		test('hiddenBefore が undefined のときは false を返す(非表示機能が有効でない)', () => {
    +			const createdAt = new Date(now - 86400000); // 1 day ago
    +			expect(shouldHideNoteByTime(undefined, createdAt)).toBe(false);
    +		});
    +	});
    +
    +	describe('相対時間モード (hiddenBefore <= 0)', () => {
    +		test('閾値内に作成されたノートは false を返す(作成からの経過時間がまだ短い→表示)', () => {
    +			const hiddenBefore = -86400; // 1 day in seconds
    +			const createdAt = new Date(now - 3600000); // 1 hour ago
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
    +		});
    +
    +		test('閾値を超えて作成されたノートは true を返す(指定期間以上経過している→非表示)', () => {
    +			const hiddenBefore = -86400; // 1 day in seconds
    +			const createdAt = new Date(now - 172800000); // 2 days ago
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
    +		});
    +
    +		test('ちょうど閾値で作成されたノートは true を返す(閾値に達したら非表示)', () => {
    +			const hiddenBefore = -86400; // 1 day in seconds
    +			const createdAt = new Date(now - 86400000); // exactly 1 day ago
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
    +		});
    +
    +		test('異なる相対時間値で判定できる(1時間設定と3時間設定の異なる結果)', () => {
    +			const createdAt = new Date(now - 7200000); // 2 hours ago
    +			expect(shouldHideNoteByTime(-3600, createdAt)).toBe(true); // 1時間経過→非表示
    +			expect(shouldHideNoteByTime(-10800, createdAt)).toBe(false); // 3時間未経過→表示
    +		});
    +
    +		test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
    +			const createdAtString = new Date(now - 86400000).toISOString();
    +			const hiddenBefore = -86400; // 1 day in seconds
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAtString)).toBe(true);
    +		});
    +
    +		test('hiddenBefore が 0 の場合に対応できる(0秒以上経過で非表示→ほぼ全て非表示)', () => {
    +			const hiddenBefore = 0;
    +			const createdAt = new Date(now - 1); // 1ms ago
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
    +		});
    +	});
    +
    +	describe('絶対時間モード (hiddenBefore > 0)', () => {
    +		test('閾値タイムスタンプより後に作成されたノートは false を返す(指定日時より後→表示)', () => {
    +			const thresholdSeconds = Math.floor(now / 1000);
    +			const createdAt = new Date(now + 3600000); // 1 hour from now
    +			expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(false);
    +		});
    +
    +		test('閾値タイムスタンプより前に作成されたノートは true を返す(指定日時より前→非表示)', () => {
    +			const thresholdSeconds = Math.floor(now / 1000);
    +			const createdAt = new Date(now - 3600000); // 1 hour ago
    +			expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
    +		});
    +
    +		test('ちょうど閾値タイムスタンプで作成されたノートは true を返す(指定日時に達したら非表示)', () => {
    +			const thresholdSeconds = Math.floor(now / 1000);
    +			const createdAt = new Date(now); // exactly now
    +			expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
    +		});
    +
    +		test('ISO 8601 形式の文字列の createdAt に対応できる(文字列でも正しく判定)', () => {
    +			const thresholdSeconds = Math.floor(now / 1000);
    +			const createdAtString = new Date(now - 3600000).toISOString();
    +			expect(shouldHideNoteByTime(thresholdSeconds, createdAtString)).toBe(true);
    +		});
    +
    +		test('異なる閾値タイムスタンプで判定できる(2021年設定と現在より1時間前設定の異なる結果)', () => {
    +			const thresholdSeconds = Math.floor((now - 86400000) / 1000); // 1 day ago
    +			const createdAtBefore = new Date(now - 172800000); // 2 days ago
    +			const createdAtAfter = new Date(now - 3600000); // 1 hour ago
    +			expect(shouldHideNoteByTime(thresholdSeconds, createdAtBefore)).toBe(true); // 閾値より前→非表示
    +			expect(shouldHideNoteByTime(thresholdSeconds, createdAtAfter)).toBe(false); // 閾値より後→表示
    +		});
    +	});
    +
    +	describe('エッジケース', () => {
    +		test('相対時間モードで非常に古いノートに対応できる(非常に古い→閾値超→非表示)', () => {
    +			const hiddenBefore = -1; // hide notes older than 1 second
    +			const createdAt = new Date(now - 1000000); // very old
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
    +		});
    +
    +		test('相対時間モードで非常に新しいノートに対応できる(非常に新しい→閾値未満→表示)', () => {
    +			const hiddenBefore = -86400; // 1 day
    +			const createdAt = new Date(now - 1); // 1ms ago
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(false);
    +		});
    +
    +		test('大きなタイムスタンプ値に対応できる(未来の日時を指定→現在のノートは全て非表示)', () => {
    +			const thresholdSeconds = Math.floor(now / 1000) + 86400; // 1 day from now
    +			const createdAt = new Date(now); // created now
    +			expect(shouldHideNoteByTime(thresholdSeconds, createdAt)).toBe(true);
    +		});
    +
    +		test('小さな相対時間値に対応できる(1秒設定で2秒前→非表示)', () => {
    +			const hiddenBefore = -1; // 1 second
    +			const createdAt = new Date(now - 2000); // 2 seconds ago
    +			expect(shouldHideNoteByTime(hiddenBefore, createdAt)).toBe(true);
    +		});
    +	});
    +});
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.