Compare commits

...

5 commits

Author SHA1 Message Date
a4540c828c
client: refactor getNoteMenu
Instead of re-determining `appearNote` inside `getNoteMenu`, it makes sense
to pass in the value since it has already been computed anyway.
2025-02-06 19:41:02 +01:00
0dfd5d8bc0
activitypub: more validation for polls 2024-11-21 19:57:24 +01:00
e384b1762b
activitypub: disallow transitive activities
This might be able to circumvent the ID host equality check, which
doesn't seem like a good idea.

Probably better since most likely the following code is not properly
equipped to handle null values anyway.
2024-11-21 19:55:52 +01:00
ce5c6f8309
server: simplify validateNote
Instead of returning an error that isn't even used, just throw the
error right away.
2024-11-21 19:55:20 +01:00
42e8cc5989
activitypub: prevent poll spoofing 2024-11-21 19:52:43 +01:00
7 changed files with 78 additions and 71 deletions

View file

@ -59,6 +59,11 @@ export default async (job: Bull.Job<InboxJobData>): Promise<string> => {
if (activity.id.length > 2048) {
return `skip: overly long id from ${signerHost}`;
}
} else {
// might want to allow null id's for transitive activites, but currently
// there are no known sensible such transitive activities that we could
// process
return "skip: non-string id";
}
// Update stats

View file

@ -29,6 +29,10 @@ export default async (actor: IRemoteUser, activity: IUpdate, resolver: Resolver)
await updatePerson(object, resolver);
return 'ok: Person updated';
} else if (getApType(object) === 'Question') {
if (actor.uri !== object.attributedTo) {
return 'skip: actor id !== question attributedTo';
}
await updateQuestion(object, resolver).catch(e => console.log(e));
return 'ok: Question updated';
} else if (isPost(object)) {

View file

@ -30,20 +30,20 @@ import { extractApMentions } from './mention.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { sideEffects } from '@/services/note/side-effects.js';
export function validateNote(object: IObject): Error | null {
function validateNote(object: IObject) {
if (object == null) {
return new Error('invalid Note: object is null');
throw new Error('invalid Note: object is null');
}
if (!isPost(object)) {
return new Error(`invalid Note: invalid object type ${getApType(object)}`);
throw new Error(`invalid Note: invalid object type ${getApType(object)}`);
}
const id = getApId(object);
if (id == null) {
// Only transient objects or anonymous objects may not have an id or an id that is explicitly null.
// We consider all Notes as not transient and not anonymous so require ids for them.
return new Error(`invalid Note: id required but not present`);
throw new Error(`invalid Note: id required but not present`);
}
// Check that the server is authorized to act on behalf of this author.
@ -52,13 +52,11 @@ export function validateNote(object: IObject): Error | null {
? extractPunyHost(getOneApId(object.attributedTo))
: null;
if (attributedToHost !== expectHost) {
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`);
throw new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${attributedToHost}`);
}
if (attributedToHost === config.hostname) {
return new Error('invalid Note: by local author');
throw new Error('invalid Note: by local author');
}
return null;
}
/**
@ -127,11 +125,7 @@ export async function fetchNote(object: string | IObject): Promise<Note | null>
export async function createNote(value: string | IObject, resolver: Resolver, silent = false): Promise<Note | null> {
const object: IObject = await resolver.resolve(value);
const err = validateNote(object);
if (err) {
apLogger.error(`${err.message}`);
throw new Error('invalid note');
}
validateNote(object);
const note: IPost = object;
@ -339,11 +333,7 @@ export async function resolveNote(value: string | IObject, resolver: Resolver):
* If the target Note is not registered, it will be ignored.
*/
export async function updateNote(value: IPost, actor: User, resolver: Resolver): Promise<Note | null> {
const err = validateNote(value);
if (err) {
apLogger.error(`${err.message}`);
throw new Error('invalid updated note');
}
validateNote(value);
const uri = getApId(value);
const exists = await Notes.findOneBy({ uri });

View file

@ -57,7 +57,7 @@ export async function updateQuestion(value: string | IObject, resolver: Resolver
const question = await resolver.resolve(value) as IQuestion;
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);
if (question.type !== 'Question') throw new Error('object is not a Question');
if (!isQuestion(question)) throw new Error('object is not a Question');
const apChoices = question.oneOf || question.anyOf;
@ -67,6 +67,10 @@ export async function updateQuestion(value: string | IObject, resolver: Resolver
const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
if (!Number.isInteger(newCount) || newcount < 0) {
throw new Error(`invalid newCount: ${newCount}`);
}
if (oldCount !== newCount) {
changed = true;
poll.votes[poll.choices.indexOf(choice)] = newCount;

View file

@ -245,12 +245,12 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), ev).then(focus);
os.contextMenu(getNoteMenu({ note: appearNote, translating, translation, menuButton, isDeleted }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), menuButton.value, {
os.popupMenu(getNoteMenu({ note: appearNote, translating, translation, menuButton, isDeleted }), menuButton.value, {
viaKeyboard,
}).then(focus);
}

View file

@ -233,12 +233,12 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
os.contextMenu(getNoteMenu({ note: appearNote, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
os.popupMenu(getNoteMenu({ note: appearNote, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
viaKeyboard,
}).then(focus);
}

View file

@ -16,9 +16,7 @@ export function getNoteMenu(props: {
isDeleted: Ref<boolean>;
currentClipPage?: Ref<foundkey.entities.Clip>;
}) {
const appearNote = foundkey.entities.isPureRenote(props.note)
? props.note.renote as foundkey.entities.Note
: props.note;
const isPureRenote = foundkey.entities.isPureRenote(props.note);
function del(): void {
os.confirm({
@ -28,7 +26,7 @@ export function getNoteMenu(props: {
if (canceled) return;
os.api('notes/delete', {
noteId: appearNote.id,
noteId: note.id,
});
});
}
@ -41,16 +39,16 @@ export function getNoteMenu(props: {
if (canceled) return;
os.api('notes/delete', {
noteId: appearNote.id,
noteId: note.id,
});
os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel });
os.post({ initialNote: note, renote: note.renote, reply: note.reply, channel: note.channel });
});
}
function toggleWatch(watch: boolean): void {
os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', {
noteId: appearNote.id,
noteId: note.id,
});
}
@ -71,7 +69,7 @@ export function getNoteMenu(props: {
}
await os.apiWithDialog('notes/thread-muting/create', {
noteId: appearNote.id,
noteId: note.id,
mutingNotificationTypes,
});
},
@ -80,23 +78,23 @@ export function getNoteMenu(props: {
function unmuteThread(): void {
os.apiWithDialog('notes/thread-muting/delete', {
noteId: appearNote.id,
noteId: note.id,
});
}
function copyContent(): void {
copyToClipboard(appearNote.text);
copyToClipboard(note.text);
os.success();
}
function copyLink(): void {
copyToClipboard(`${url}/notes/${appearNote.id}`);
copyToClipboard(`${url}/notes/${note.id}`);
os.success();
}
function togglePin(pin: boolean): void {
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
noteId: appearNote.id,
noteId: note.id,
}, undefined, null, res => {
if (res.code === 'PIN_LIMIT_EXCEEDED') {
os.alert({
@ -134,13 +132,13 @@ export function getNoteMenu(props: {
const clip = await os.apiWithDialog('clips/create', result);
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id });
os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: note.id });
},
}, null, ...clips.map(clip => ({
text: clip.name,
action: () => {
os.promiseDialog(
os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }),
os.api('clips/add-note', { clipId: clip.id, noteId: note.id }),
null,
async (err) => {
if (err.id === 'ALREADY_CLIPPED') {
@ -149,7 +147,7 @@ export function getNoteMenu(props: {
text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }),
});
if (!confirm.canceled) {
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: note.id });
if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
}
} else {
@ -166,15 +164,15 @@ export function getNoteMenu(props: {
}
async function unclip(): Promise<void> {
os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id });
os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: note.id });
props.isDeleted.value = true;
}
function share(): void {
navigator.share({
title: i18n.t('noteOf', { user: appearNote.user.name || appearNote.user.username }),
text: appearNote.text,
url: `${url}/notes/${appearNote.id}`,
title: i18n.t('noteOf', { user: note.user.name || note.user.username }),
text: note.text,
url: `${url}/notes/${note.id}`,
});
}
@ -190,7 +188,7 @@ export function getNoteMenu(props: {
}
const res = await os.api('notes/translate', {
noteId: appearNote.id,
noteId: note.id,
targetLang,
});
props.translating.value = false;
@ -200,7 +198,7 @@ export function getNoteMenu(props: {
let menu;
if ($i) {
const statePromise = os.api('notes/state', {
noteId: appearNote.id,
noteId: note.id,
});
menu = [
@ -212,19 +210,21 @@ export function getNoteMenu(props: {
action: unclip,
}, null] : []
),
{
!isPureRenote ? {
icon: 'fas fa-copy',
text: i18n.ts.copyContent,
action: copyContent,
}, {
} : undefined,
{
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: copyLink,
}, (appearNote.url || appearNote.uri) ? {
},
(note.url || note.uri) ? {
icon: 'fas fa-external-link-square-alt',
text: i18n.ts.showOnRemote,
action: () => {
window.open(appearNote.url || appearNote.uri, '_blank');
window.open(note.url || note.uri, '_blank');
},
} : undefined,
{
@ -232,7 +232,7 @@ export function getNoteMenu(props: {
text: i18n.ts.share,
action: share,
},
instance.translatorAvailable ? {
(!isPureRenote && instance.translatorAvailable) ? {
icon: 'fas fa-language',
text: i18n.ts.translate,
action: translate,
@ -243,7 +243,7 @@ export function getNoteMenu(props: {
text: i18n.ts.clip,
action: () => clip(),
},
(appearNote.userId !== $i.id) ? statePromise.then(state => state.isWatching ? {
(note.userId !== $i.id) ? statePromise.then(state => state.isWatching ? {
icon: 'fas fa-eye-slash',
text: i18n.ts.unwatch,
action: () => toggleWatch(false),
@ -261,7 +261,7 @@ export function getNoteMenu(props: {
text: i18n.ts.muteThread,
action: () => muteThread(),
}),
appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
note.userId === $i.id ? ($i.pinnedNoteIds || []).includes(note.id) ? {
icon: 'fas fa-thumbtack',
text: i18n.ts.unpin,
action: () => togglePin(false),
@ -270,24 +270,24 @@ export function getNoteMenu(props: {
text: i18n.ts.pin,
action: () => togglePin(true),
} : undefined,
...(appearNote.userId !== $i.id ? [
...(note.userId !== $i.id && !isPureRenote ? [
null,
{
icon: 'fas fa-exclamation-circle',
text: i18n.ts.reportAbuse,
action: () => {
const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`;
const u = note.url || note.uri || `${url}/notes/${note.id}`;
os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), {
user: appearNote.user,
user: note.user,
urls: [u],
}, {}, 'closed');
},
}]
: []
),
...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [
...(note.userId === $i.id || $i.isModerator || $i.isAdmin ? [
null,
appearNote.userId === $i.id ? {
(!isPureRenote && note.userId === $i.id) ? {
icon: 'fas fa-edit',
text: i18n.ts.deleteAndEdit,
action: delEdit,
@ -302,21 +302,25 @@ export function getNoteMenu(props: {
)]
.filter(x => x !== undefined);
} else {
menu = [{
icon: 'fas fa-copy',
text: i18n.ts.copyContent,
action: copyContent,
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: copyLink,
}, (appearNote.url || appearNote.uri) ? {
icon: 'fas fa-external-link-square-alt',
text: i18n.ts.showOnRemote,
action: () => {
window.open(appearNote.url || appearNote.uri, '_blank');
menu = [
!isPureRenote ? {
icon: 'fas fa-copy',
text: i18n.ts.copyContent,
action: copyContent,
} : undefined,
{
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: copyLink,
},
} : undefined]
(note.url || note.uri) ? {
icon: 'fas fa-external-link-square-alt',
text: i18n.ts.showOnRemote,
action: () => {
window.open(note.url || note.uri, '_blank');
},
} : undefined
]
.filter(x => x !== undefined);
}
@ -325,7 +329,7 @@ export function getNoteMenu(props: {
icon: 'fas fa-plug',
text: action.title,
action: () => {
action.handler(appearNote);
action.handler(note);
},
}))]);
}