summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-09-21 11:58:51 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-09-21 11:58:51 +0900
commite41619775f0ab57d67d4f49637768417730d68c0 (patch)
tree1a9befa1c29711ab717ae164e57c133a47218256
parent:art: (diff)
downloadsharkey-e41619775f0ab57d67d4f49637768417730d68c0.tar.gz
sharkey-e41619775f0ab57d67d4f49637768417730d68c0.tar.bz2
sharkey-e41619775f0ab57d67d4f49637768417730d68c0.zip
feat: プロフィールでのリンク検証
Resolve #11099
-rw-r--r--CHANGELOG.md1
-rw-r--r--locales/index.d.ts1
-rw-r--r--locales/ja-JP.yml1
-rw-r--r--packages/backend/migration/1695260774117-verified-links.js11
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts1
-rw-r--r--packages/backend/src/models/UserProfile.ts6
-rw-r--r--packages/backend/src/models/json-schema/user.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts55
-rw-r--r--packages/backend/test/e2e/users.ts2
-rw-r--r--packages/frontend/.storybook/fakes.ts1
-rw-r--r--packages/frontend/src/pages/user/home.vue10
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md1
-rw-r--r--packages/misskey-js/src/entities.ts1
13 files changed, 93 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2492b187ac..18c5d5f558 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -26,6 +26,7 @@
- Feat: 二要素認証のバックアップコードが生成されるようになりました
- ref. https://github.com/MisskeyIO/misskey/pull/121
- Feat: 二要素認証でパスキーをサポートするようになりました
+- Feat: プロフィールでのリンク検証
- Feat: 通知をテストできるようになりました
- Feat: PWAのアイコンが設定できるようになりました
- Enhance: manifest.jsonをオーバーライド可能に
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 649b0be44a..bd1f10d86e 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1116,6 +1116,7 @@ export interface Locale {
"loadConversation": string;
"pinnedList": string;
"keepScreenOn": string;
+ "verifiedLink": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 69228ed17e..8e684111fd 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1113,6 +1113,7 @@ loadReplies: "返信を見る"
loadConversation: "会話を見る"
pinnedList: "ピン留めされたリスト"
keepScreenOn: "デバイスの画面を常にオンにする"
+verifiedLink: "このリンク先の所有者であることが確認されました"
_announcement:
forExistingUsers: "既存ユーザーのみ"
diff --git a/packages/backend/migration/1695260774117-verified-links.js b/packages/backend/migration/1695260774117-verified-links.js
new file mode 100644
index 0000000000..18e0571d81
--- /dev/null
+++ b/packages/backend/migration/1695260774117-verified-links.js
@@ -0,0 +1,11 @@
+export class VerifiedLinks1695260774117 {
+ name = 'VerifiedLinks1695260774117'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" ADD "verifiedLinks" character varying array NOT NULL DEFAULT '{}'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "verifiedLinks"`);
+ }
+}
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index c0909a663d..7bef410bf9 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -384,6 +384,7 @@ export class UserEntityService implements OnModuleInit {
birthday: profile!.birthday,
lang: profile!.lang,
fields: profile!.fields,
+ verifiedLinks: profile!.verifiedLinks,
followersCount: followersCount ?? 0,
followingCount: followingCount ?? 0,
notesCount: user.notesCount,
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 6c7ffe4c39..e4405c9da7 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -49,6 +49,12 @@ export class MiUserProfile {
}[];
@Column('varchar', {
+ array: true,
+ default: '{}',
+ })
+ public verifiedLinks: string[];
+
+ @Column('varchar', {
length: 32, nullable: true,
})
public lang: string | null;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 3314464c31..8d0e4e72ed 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -169,6 +169,15 @@ export const packedUserDetailedNotMeOnlySchema = {
},
},
},
+ verifiedLinks: {
+ type: 'array',
+ nullable: false, optional: false,
+ items: {
+ type: 'string',
+ nullable: false, optional: false,
+ format: 'url',
+ },
+ },
followersCount: {
type: 'number',
nullable: false, optional: false,
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 3953b19002..b11e091957 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -6,11 +6,13 @@
import RE2 from 're2';
import * as mfm from 'mfm-js';
import { Inject, Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
-import type { MiUser } from '@/models/User.js';
+import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
import { notificationTypes } from '@/types.js';
@@ -27,6 +29,9 @@ import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import { HttpRequestService } from '@/core/HttpRequestService.js';
+import type { Config } from '@/config.js';
+import { safeForSql } from '@/misc/safe-for-sql.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import { ApiError } from '../../error.js';
@@ -37,6 +42,11 @@ export const meta = {
kind: 'write:account',
+ limit: {
+ duration: ms('1hour'),
+ max: 10,
+ },
+
errors: {
noSuchAvatar: {
message: 'No such avatar file.',
@@ -173,6 +183,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -195,9 +208,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private hashtagService: HashtagService,
private roleService: RoleService,
private cacheService: CacheService,
+ private httpRequestService: HttpRequestService,
) {
super(meta, paramDef, async (ps, _user, token) => {
- const user = await this.usersRepository.findOneByOrFail({ id: _user.id });
+ const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
const isSecure = token == null;
const updates = {} as Partial<MiUser>;
@@ -296,9 +310,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.fields) {
profileUpdates.fields = ps.fields
- .filter(x => typeof x.name === 'string' && x.name !== '' && typeof x.value === 'string' && x.value !== '')
+ .filter(x => typeof x.name === 'string' && x.name.trim() !== '' && typeof x.value === 'string' && x.value.trim() !== '')
.map(x => {
- return { name: x.name, value: x.value };
+ return { name: x.name.trim(), value: x.value.trim() };
});
}
@@ -364,7 +378,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Object.keys(updates).includes('alsoKnownAs')) {
this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
}
- if (Object.keys(profileUpdates).length > 0) await this.userProfilesRepository.update(user.id, profileUpdates);
+
+ await this.userProfilesRepository.update(user.id, {
+ ...profileUpdates,
+ verifiedLinks: [],
+ });
const iObj = await this.userEntityService.pack<true, true>(user.id, user, {
detail: true,
@@ -386,7 +404,34 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// フォロワーにUpdateを配信
this.accountUpdateService.publishToFollowers(user.id);
+ const urls = updatedProfile.fields.filter(x => x.value.startsWith('https://'));
+ for (const url of urls) {
+ this.verifyLink(url.value, user);
+ }
+
return iObj;
});
}
+
+ private async verifyLink(url: string, user: MiLocalUser) {
+ if (!safeForSql(url)) return;
+
+ const html = await this.httpRequestService.getHtml(url);
+
+ const { window } = new JSDOM(html);
+ const doc = window.document;
+
+ const myLink = `${this.config.url}/@${user.username}`;
+
+ const includesMyLink = Array.from(doc.getElementsByTagName('a')).some(a => a.href === myLink);
+
+ if (includesMyLink) {
+ await this.userProfilesRepository.createQueryBuilder('profile').update()
+ .where('userId = :userId', { userId: user.id })
+ .set({
+ verifiedLinks: () => `array_append("verifiedLinks", '${url}')`, // ここでSQLインジェクションされそうなのでとりあえず safeForSql で弾いている
+ })
+ .execute();
+ }
+ }
}
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 2c396813ff..13cea0cfc2 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -102,6 +102,7 @@ describe('ユーザー', () => {
birthday: user.birthday,
lang: user.lang,
fields: user.fields,
+ verifiedLinks: user.verifiedLinks,
followersCount: user.followersCount,
followingCount: user.followingCount,
notesCount: user.notesCount,
@@ -369,6 +370,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.birthday, null);
assert.strictEqual(response.lang, null);
assert.deepStrictEqual(response.fields, []);
+ assert.deepStrictEqual(response.verifiedLinks, []);
assert.strictEqual(response.followersCount, 0);
assert.strictEqual(response.followingCount, 0);
assert.strictEqual(response.notesCount, 0);
diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts
index 14481deeea..2bda89196a 100644
--- a/packages/frontend/.storybook/fakes.ts
+++ b/packages/frontend/.storybook/fakes.ts
@@ -89,6 +89,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
value: 'https://misskey-hub.net',
},
],
+ verifiedLinks: [],
followersCount: 1024,
followingCount: 16,
hasPendingFollowRequestFromYou: false,
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 8195c40bf9..385c81a97f 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -101,6 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</dt>
<dd class="value">
<Mfm :text="field.value" :author="user" :i="$i" :colored="false"/>
+ <i v-if="user.verifiedLinks.includes(field.value)" v-tooltip:dialog="i18n.ts.verifiedLink" class="ti ti-circle-check" :class="$style.verifiedLink"></i>
</dd>
</dl>
</div>
@@ -671,7 +672,12 @@ onUnmounted(() => {
<style lang="scss" module>
.tl {
background: var(--bg);
- border-radius: var(--radius);
- overflow: clip;
+ border-radius: var(--radius);
+ overflow: clip;
+}
+
+.verifiedLink {
+ margin-left: 4px;
+ color: var(--success);
}
</style>
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 99b3852b02..fd2d0ced02 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -2778,6 +2778,7 @@ type UserDetailed = UserLite & {
name: string;
value: string;
}[];
+ verifiedLinks: string[];
followersCount: number;
followingCount: number;
hasPendingFollowRequestFromYou: boolean;
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 2876339102..018210c96b 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -38,6 +38,7 @@ export type UserDetailed = UserLite & {
description: string | null;
ffVisibility: 'public' | 'followers' | 'private';
fields: {name: string; value: string}[];
+ verifiedLinks: string[];
followersCount: number;
followingCount: number;
hasPendingFollowRequestFromYou: boolean;