Compare commits

...

10 commits

Author SHA1 Message Date
zenja
dab22cd631
locales/de-DE.yml aktualisiert
Some checks are pending
/ test-build (push) Waiting to run
fixed name issues
2024-11-27 23:11:16 +01:00
Laura Hausmann
4e61f25d41
[backend] Bump msgpackr version
This fixes compatibility with NodeJS v23+
2024-11-25 22:47:38 +01:00
mia
bd1bb68da3
[backend] Bump re2
Fixes some build issues
2024-11-24 10:07:52 -08:00
Laura Hausmann
617f27d637
Release: v2023.12.11 2024-11-20 23:56:38 +01:00
Kopper
a5f4279d32
[backend] Check target IP before sending HTTP request
Backported upstream commit "fix(backend): check target IP before sending HTTP request"

Co-authored-by: rectcoordsystem <heohyun73@gmail.com>
Co-authored-by: anatawa12 <anatawa12@icloud.com>
2024-11-20 23:56:37 +01:00
Laura Hausmann
065590279e
[backend] Strengthen checks against local object resolution
This commit addresses disclosed primitives 26-29 & 31-33
2024-11-20 23:56:20 +01:00
Laura Hausmann
ca331d2406
[backend] Create a new resolver in parseAudience if none is passed to the function
This commit addresses disclosed primitive 23
2024-11-20 23:56:16 +01:00
Laura Hausmann
dc3c2d1ad4
[backend] Enforce blocks in NoteRepository.isVisibleForMe
This commit addresses disclosed primitive 20
2024-11-20 23:56:12 +01:00
Laura Hausmann
aa73a8905d
[backend] Require admin scope for AP get endpoint
This commit addresses disclosed primitive 18
2024-11-20 23:56:07 +01:00
Laura Hausmann
7542310e3e
[backend] Improve validation of AP activities & objects
This commit addresses disclosed primitives 4-5, 7-9, 12-17 & 21-22 (CVE-2024-51403, CVE-2024-51404, CVE-2024-51405)
2024-11-20 23:56:02 +01:00
37 changed files with 327 additions and 194 deletions

55
.pnp.cjs generated
View file

@ -7295,7 +7295,7 @@ const RAW_RUNTIME_STATE =
["mfm-js", "npm:0.23.3"],\
["mime-types", "npm:2.1.35"],\
["mocha", "npm:10.2.0"],\
["msgpackr", "npm:1.9.5"],\
["msgpackr", "npm:1.11.2"],\
["multer", "npm:1.4.4-lts.1"],\
["nested-property", "npm:4.0.0"],\
["node-fetch", "npm:3.3.2"],\
@ -7317,7 +7317,7 @@ const RAW_RUNTIME_STATE =
["qs", "npm:6.11.2"],\
["random-seed", "npm:0.3.0"],\
["ratelimiter", "npm:3.4.1"],\
["re2", "npm:1.20.11"],\
["re2", "npm:1.21.4"],\
["redis-lock", "npm:0.1.4"],\
["redis-semaphore", "virtual:aa59773ac87791c4813d53447077fcf8a847d6de5a301d34dc31286584b1dbb26d30d3adb5b4c41c1e8aea04371e926fda05c09c6253647c432e11d872a304ba#npm:5.3.1"],\
["reflect-metadata", "npm:0.1.13"],\
@ -18152,10 +18152,10 @@ const RAW_RUNTIME_STATE =
],\
"linkType": "HARD"\
}],\
["npm:1.9.5", {\
"packageLocation": "./.yarn/cache/msgpackr-npm-1.9.5-69f0e8f5b8-d95fbee39b.zip/node_modules/msgpackr/",\
["npm:1.11.2", {\
"packageLocation": "./.yarn/cache/msgpackr-npm-1.11.2-a21c5db6f8-7602f1e91e.zip/node_modules/msgpackr/",\
"packageDependencies": [\
["msgpackr", "npm:1.9.5"],\
["msgpackr", "npm:1.11.2"],\
["msgpackr-extract", "npm:3.0.2"]\
],\
"linkType": "HARD"\
@ -18225,10 +18225,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["nan", [\
["npm:2.19.0", {\
"packageLocation": "./.yarn/unplugged/nan-npm-2.19.0-2f5da4a528/node_modules/nan/",\
["npm:2.22.0", {\
"packageLocation": "./.yarn/unplugged/nan-npm-2.22.0-3750ad85d9/node_modules/nan/",\
"packageDependencies": [\
["nan", "npm:2.19.0"],\
["nan", "npm:2.22.0"],\
["node-gyp", "npm:9.4.0"]\
],\
"linkType": "HARD"\
@ -18406,19 +18406,19 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["node-gyp", [\
["npm:10.0.1", {\
"packageLocation": "./.yarn/unplugged/node-gyp-npm-10.0.1-48708ce70b/node_modules/node-gyp/",\
["npm:10.2.0", {\
"packageLocation": "./.yarn/unplugged/node-gyp-npm-10.2.0-cad1109948/node_modules/node-gyp/",\
"packageDependencies": [\
["node-gyp", "npm:10.0.1"],\
["node-gyp", "npm:10.2.0"],\
["env-paths", "npm:2.2.1"],\
["exponential-backoff", "npm:3.1.1"],\
["glob", "npm:10.3.10"],\
["graceful-fs", "npm:4.2.11"],\
["make-fetch-happen", "npm:13.0.0"],\
["nopt", "npm:7.2.0"],\
["proc-log", "npm:3.0.0"],\
["proc-log", "npm:4.2.0"],\
["semver", "npm:7.5.4"],\
["tar", "npm:6.1.15"],\
["tar", "npm:6.2.1"],\
["which", "npm:4.0.0"]\
],\
"linkType": "HARD"\
@ -20445,10 +20445,10 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["proc-log", [\
["npm:3.0.0", {\
"packageLocation": "./.yarn/cache/proc-log-npm-3.0.0-a8c21c2f0f-02b64e1b39.zip/node_modules/proc-log/",\
["npm:4.2.0", {\
"packageLocation": "./.yarn/cache/proc-log-npm-4.2.0-4d65296a9d-4e1394491b.zip/node_modules/proc-log/",\
"packageDependencies": [\
["proc-log", "npm:3.0.0"]\
["proc-log", "npm:4.2.0"]\
],\
"linkType": "HARD"\
}]\
@ -20975,13 +20975,13 @@ const RAW_RUNTIME_STATE =
}]\
]],\
["re2", [\
["npm:1.20.11", {\
"packageLocation": "./.yarn/unplugged/re2-npm-1.20.11-ab65de125e/node_modules/re2/",\
["npm:1.21.4", {\
"packageLocation": "./.yarn/unplugged/re2-npm-1.21.4-315af4327e/node_modules/re2/",\
"packageDependencies": [\
["re2", "npm:1.20.11"],\
["re2", "npm:1.21.4"],\
["install-artifact-from-github", "npm:1.3.5"],\
["nan", "npm:2.19.0"],\
["node-gyp", "npm:10.0.1"]\
["nan", "npm:2.22.0"],\
["node-gyp", "npm:10.2.0"]\
],\
"linkType": "HARD"\
}]\
@ -23165,6 +23165,19 @@ const RAW_RUNTIME_STATE =
["yallist", "npm:4.0.0"]\
],\
"linkType": "HARD"\
}],\
["npm:6.2.1", {\
"packageLocation": "./.yarn/cache/tar-npm-6.2.1-237800bb20-bfbfbb2861.zip/node_modules/tar/",\
"packageDependencies": [\
["tar", "npm:6.2.1"],\
["chownr", "npm:2.0.0"],\
["fs-minipass", "npm:2.1.0"],\
["minipass", "npm:5.0.0"],\
["minizlib", "npm:2.1.2"],\
["mkdirp", "npm:1.0.4"],\
["yallist", "npm:4.0.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["tar-stream", [\

BIN
.yarn/cache/msgpackr-npm-1.11.2-a21c5db6f8-7602f1e91e.zip (Stored with Git LFS) vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
.yarn/cache/nan-npm-2.19.0-2f5da4a528-b97f680753.zip (Stored with Git LFS) vendored

Binary file not shown.

BIN
.yarn/cache/nan-npm-2.22.0-3750ad85d9-ab165ba910.zip (Stored with Git LFS) vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
.yarn/cache/node-gyp-npm-10.2.0-cad1109948-41773093b1.zip (Stored with Git LFS) vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
.yarn/cache/proc-log-npm-4.2.0-4d65296a9d-4e1394491b.zip (Stored with Git LFS) vendored Normal file

Binary file not shown.

BIN
.yarn/cache/re2-npm-1.20.11-ab65de125e-a8665c861c.zip (Stored with Git LFS) vendored

Binary file not shown.

BIN
.yarn/cache/re2-npm-1.21.4-315af4327e-926871cc84.zip (Stored with Git LFS) vendored Normal file

Binary file not shown.

BIN
.yarn/cache/tar-npm-6.2.1-237800bb20-bfbfbb2861.zip (Stored with Git LFS) vendored Normal file

Binary file not shown.

View file

@ -1,3 +1,23 @@
## v2023.12.11
This release contains several critical security patches, as well as minor fixes and improvements. Upgrading is strongly recommended for all server operators.
### Highlights
- Several DoS, impersonation, data leakage & click jacking vulnerabilities have been patched
### Backend
- Various issues related to AP object validation have been resolved
- The ap/get API endpoint is now only available to administrators
- Blocks are now enforced in NoteRepository.isVisibleForMe
- Audience parsing no longer bypasses the AP recursion limit
- Edits of local-only notes are no longer federated out
- AP object URIs now get canonicalized before comparing them for consistency
- SSRF prevention now applies to all code paths
### Attribution
This release was made possible by project contributors: Kopper & Laura Hausmann
Furthermore, I want to give special thanks to Hazel Koehler for the vulnerability disclosure.
## v2023.12.10
This release contains a critical security patch, as well as minor fixes and improvements. Upgrading is strongly recommended for all server operators.

View file

@ -1086,7 +1086,7 @@ _registry:
domain: "Domain"
createKey: "Schlüssel erstellen"
_aboutIceshrimp:
about: "Iceshrimp ist ein Fork von Iceshrimp, der seit 2022 von ThatOneCalculator
about: "Iceshrimp ist ein Fork von Firefish, der seit 2022 von zotan
entwickelt wird."
contributors: "Hauptmitwirkende"
allContributors: "Alle Mitwirkenden"

View file

@ -1,6 +1,6 @@
{
"name": "iceshrimp",
"version": "2023.12.10",
"version": "2023.12.11",
"repository": {
"type": "git",
"url": "https://iceshrimp.dev/iceshrimp/iceshrimp.git"

View file

@ -90,7 +90,7 @@
"koa-views": "7.0.2",
"mfm-js": "0.23.3",
"mime-types": "2.1.35",
"msgpackr": "1.9.5",
"msgpackr": "1.11.2",
"multer": "1.4.4-lts.1",
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
@ -111,7 +111,7 @@
"qs": "6.11.2",
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "^1.20.11",
"re2": "^1.21.4",
"redis-lock": "0.1.4",
"redis-semaphore": "5.3.1",
"reflect-metadata": "0.1.13",

View file

@ -0,0 +1,60 @@
import * as http from "node:http";
import * as https from "node:https";
import net from "node:net";
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
import config from "@/config/index.js";
import IPCIDR from "ip-cidr";
import PrivateIp from "private-ip";
declare module 'node:http' {
interface Agent {
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket;
}
}
function isPrivateIp(ip: string): boolean {
for (const net of config.allowedPrivateNetworks || []) {
const cidr = new IPCIDR(net);
if (cidr.contains(ip)) {
return false;
}
}
return PrivateIp(ip);
}
function checkConnection(socket: net.Socket) {
const address = socket.remoteAddress;
if (process.env.NODE_ENV === 'production') {
if (address && IPCIDR.isValidAddress(address) && isPrivateIp(address)) {
socket.destroy(new Error(`Blocked address: ${address}`));
}
}
}
export class CheckedHttpAgent extends http.Agent {
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
return socket;
}
}
export class CheckedHttpsAgent extends https.Agent {
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
return socket;
}
}
export class CheckedHttpProxyAgent extends HttpProxyAgent {
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
return socket;
}
}
export class CheckedHttpsProxyAgent extends HttpsProxyAgent {
createConnection(options: net.NetConnectOpts, callback?: (err: unknown, stream: net.Socket) => void): net.Socket {
const socket = super.createConnection(options, callback).on('connect', () => { checkConnection(socket) });
return socket;
}
}

View file

@ -6,8 +6,6 @@ import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
import config from "@/config/index.js";
import chalk from "chalk";
import Logger from "@/services/logger.js";
import IPCIDR from "ip-cidr";
import PrivateIp from "private-ip";
const pipeline = util.promisify(stream.pipeline);
@ -45,18 +43,6 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
},
})
.on("response", (res: Got.Response) => {
if (
(process.env.NODE_ENV === "production" ||
process.env.NODE_ENV === "test") &&
!config.proxy &&
res.ip
) {
if (isPrivateIp(res.ip)) {
logger.warn(`Blocked address: ${res.ip}`);
req.destroy();
}
}
const contentLength = res.headers["content-length"];
if (contentLength != null) {
const size = Number(contentLength);
@ -92,13 +78,3 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
logger.succ(`Download finished: ${chalk.cyan(url)}`);
}
function isPrivateIp(ip: string): boolean {
for (const net of config.allowedPrivateNetworks || []) {
const cidr = new IPCIDR(net);
if (cidr.contains(ip)) {
return false;
}
}
return PrivateIp(ip);
}

View file

@ -3,8 +3,9 @@ import * as https from "node:https";
import type { URL } from "node:url";
import CacheableLookup from "cacheable-lookup";
import fetch from "node-fetch";
import { HttpProxyAgent, HttpsProxyAgent } from "hpagent";
import config from "@/config/index.js";
import net from "node:net";
import {CheckedHttpAgent, CheckedHttpProxyAgent, CheckedHttpsAgent, CheckedHttpsProxyAgent} from "@/misc/checked-fetch.js";
export async function getJson(
url: string,
@ -132,7 +133,7 @@ const cache = new CacheableLookup({
/**
* Get http non-proxy agent
*/
const _http = new http.Agent({
const _http = new CheckedHttpAgent({
keepAlive: true,
keepAliveMsecs: 30 * 1000,
lookup: cache.lookup,
@ -141,7 +142,7 @@ const _http = new http.Agent({
/**
* Get https non-proxy agent
*/
const _https = new https.Agent({
const _https = new CheckedHttpsAgent({
keepAlive: true,
keepAliveMsecs: 30 * 1000,
lookup: cache.lookup,
@ -153,7 +154,7 @@ const maxSockets = Math.max(256, config.deliverJobConcurrency || 128);
* Get http proxy or non-proxy agent
*/
export const httpAgent = config.proxy
? new HttpProxyAgent({
? new CheckedHttpProxyAgent({
keepAlive: true,
keepAliveMsecs: 30 * 1000,
maxSockets,
@ -167,7 +168,7 @@ export const httpAgent = config.proxy
* Get https proxy or non-proxy agent
*/
export const httpsAgent = config.proxy
? new HttpsProxyAgent({
? new CheckedHttpsProxyAgent({
keepAlive: true,
keepAliveMsecs: 30 * 1000,
maxSockets,

View file

@ -10,7 +10,7 @@ import {
Followings,
Polls,
Channels,
Notes, UserProfiles,
Notes, UserProfiles, Blockings,
} from "../index.js";
import type { Packed } from "@/misc/schema.js";
import { nyaize } from "@/misc/nyaize.js";
@ -113,6 +113,20 @@ async function populateIsRenoted(
export const NoteRepository = db.getRepository(Note).extend({
async isVisibleForMe(note: Note, meId: User["id"] | null): Promise<boolean> {
if (meId != null && meId !== note.userId) {
const blocked = await Blockings.count({
where: {
blockeeId: meId,
blockerId: note.userId
},
take: 1
});
if (blocked !== 0) {
return false;
}
}
// This code must always be synchronized with the checks in generateVisibilityQuery.
// visibility が specified かつ自分が指定されていなかったら非表示
if (note.visibility === "specified") {

View file

@ -178,12 +178,14 @@ async function process(job: Job<InboxJobData>): Promise<string> {
}
// activity.idがあればホストが署名者のホストであることを確認する
if (typeof activity.id === "string") {
const signerHost = extractDbHost(authUser.user.uri!);
const activityIdHost = extractDbHost(activity.id);
if (signerHost !== activityIdHost) {
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
}
if (typeof activity.id !== "string") {
return 'skip: activity.id is not a string';
}
const signerHost = extractDbHost(authUser.user.uri!);
const activityIdHost = extractDbHost(activity.id);
if (signerHost !== activityIdHost) {
return `skip: signerHost(${signerHost}) !== activity.id host(${activityIdHost}`;
}
// Update stats

View file

@ -1,6 +1,6 @@
import type { ApObject } from "./type.js";
import { getApIds } from "./type.js";
import type Resolver from "./resolver.js";
import Resolver from "./resolver.js";
import { resolvePerson } from "./models/person.js";
import { unique, concat } from "@/prelude/array.js";
import promiseLimit from "promise-limit";
@ -31,6 +31,7 @@ export async function parseAudience(
const others = unique(concat([toGroups.other, ccGroups.other]));
resolver ??= new Resolver();
const limit = promiseLimit<CacheableUser | null>(2);
const mentionedUsers = (
await Promise.all(

View file

@ -37,7 +37,7 @@ export async function checkFetch(req: IncomingMessage): Promise<number> {
let signature;
try {
signature = httpSignature.parseRequest(req, { headers: ["(request-target)", "host", "date"] });
signature = httpSignature.parseRequest(req, { headers: ["(request-target)", "host", "date"], authorizationHeaderName: 'signature' });
} catch (e) {
return 401;
}

View file

@ -19,6 +19,7 @@ import type { IObject } from "./type.js";
import { getApId } from "./type.js";
import { resolvePerson, updatePerson } from "./models/person.js";
import {redisClient, subscriber} from "@/db/redis.js";
import { extractDbHost, toPuny } from "@/misc/convert-host.js";
const publicKeyCache = new Cache<UserPublickey | null>("publicKey", 60 * 30);
const publicKeyByUserIdCache = new Cache<UserPublickey | null>(
@ -46,15 +47,15 @@ export type UriParseResult =
export function parseUri(value: string | IObject): UriParseResult {
const uri = getApId(value);
const parsed = new URL(uri);
// the host part of a URL is case insensitive, so use the 'i' flag.
const localRegex = new RegExp(
`^${escapeRegexp(config.url)}/(\\w+)/(\\w+)(?:/(.+))?`,
"i",
);
const matchLocal = uri.match(localRegex);
if (toPuny(parsed.host) === toPuny(config.host)) {
const localRegex = new RegExp(`^.*?/(\\w+)/(\\w+)(?:/(.+))?`);
const matchLocal = uri.match(localRegex);
if (matchLocal == null) {
throw new Error(`Failed to parse local URI: ${uri}`);
}
if (matchLocal) {
return {
local: true,
type: matchLocal[1],

View file

@ -29,6 +29,9 @@ export default async function (
return "skip: host in actor.uri !== note.id";
}
}
else {
return "skip: note.id is not a string";
}
}
const unlock = await getApLock(uri);

View file

@ -46,19 +46,8 @@ export async function performActivity(
activity: IObject,
) {
if (isCollectionOrOrderedCollection(activity)) {
const resolver = new Resolver();
for (const item of toArray(
isCollection(activity) ? activity.items : activity.orderedItems,
)) {
const act = await resolver.resolve(item);
try {
await performOneActivity(actor, act);
} catch (err) {
if (err instanceof Error || typeof err === "string") {
apLogger.error(err);
}
}
}
apLogger.debug('Refusing to ingest collection as activity');
return;
} else {
await performOneActivity(actor, activity);
}

View file

@ -1,5 +1,5 @@
import type { CacheableRemoteUser } from "@/models/entities/user.js";
import type { IUpdate } from "../../type.js";
import { getApId, IUpdate } from "../../type.js";
import { getApType, isActor } from "../../type.js";
import { apLogger } from "../../logger.js";
import { updateNote } from "../../models/note.js";
@ -13,7 +13,7 @@ export default async (
actor: CacheableRemoteUser,
activity: IUpdate,
): Promise<string> => {
if ("actor" in activity && actor.uri !== activity.actor) {
if (actor.uri == null || actor.uri !== getApId(activity.actor)) {
return "skip: invalid actor";
}
@ -27,6 +27,10 @@ export default async (
});
if (isActor(object)) {
if (actor.uri !== object.id) {
return "skip: actor id mismatch";
}
await updatePerson(actor.uri!, resolver, object);
return "ok: Person updated";
}
@ -39,7 +43,7 @@ export default async (
case "Document":
case "Page":
let failed = false;
await updateNote(object, resolver).catch((e: Error) => {
await updateNote(object, actor, resolver).catch((e: Error) => {
failed = true;
});
return failed ? "skip: Note update failed" : "ok: Note updated";

View file

@ -131,13 +131,20 @@ export async function createNote(
const note: IPost = object;
if (note.id && !note.id.startsWith("https://")) {
if (note.id == null) {
throw new Error('Note must have an id');
}
const idUrl = new URL(note.id);
if (idUrl.protocol != 'https:') {
throw new Error(`unexpected schema of note.id: ${note.id}`);
}
const url = getOneApHrefNullable(note.url);
let url = getOneApHrefNullable(note.url);
const urlUrl = url != null ? new URL(url) : null;
if (url && !url.startsWith("https://")) {
if (urlUrl != null && urlUrl.protocol != 'https:') {
throw new Error(`unexpected schema of note url: ${url}`);
}
@ -169,6 +176,22 @@ export async function createNote(
limiter
)) as CacheableRemoteUser;
if (actor.uri == null) {
logger.warn('Note actor uri is null, discarding');
return null;
}
const actorUri = new URL(actor.uri);
if (idUrl.host != actorUri.host) {
logger.warn("Note id host doesn't match actor host, discarding");
return null;
}
if (urlUrl != null && urlUrl.host != actorUri.host) {
logger.debug("Note url host doesn't match actor host, clearing variable");
url = undefined;
}
// Skip if author is suspended.
if (actor.isSuspended) {
logger.debug(
@ -432,7 +455,7 @@ export async function resolveNote(
}
//#endregion
if (uri.startsWith(config.url)) {
if (extractDbHost(uri) === toPuny(config.host)) {
throw new StatusError(
"cannot resolve local note",
400,
@ -544,12 +567,12 @@ function notEmpty(partial: Partial<any>) {
return Object.keys(partial).length > 0;
}
export async function updateNote(value: string | IObject, resolver?: Resolver) {
export async function updateNote(value: string | IObject, actor: CacheableRemoteUser, resolver?: Resolver) {
const uri = typeof value === "string" ? value : value.id;
if (!uri) throw new Error("Missing note uri");
// Skip if URI points to this server
if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local");
if (extractDbHost(uri) === toPuny(config.host)) throw new Error("uri points local");
// A new resolver is created if not specified
if (resolver == null) resolver = new Resolver();
@ -557,16 +580,18 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
// Resolve the updated Note object
const post = (await resolver.resolve(value)) as IPost;
const actor = (await resolvePerson(
getOneApId(post.attributedTo),
resolver,
)) as CacheableRemoteUser;
if (getOneApId(post.attributedTo) !== actor.uri || actor.uri == null) {
throw new Error('Refusing to ingest update for note with mismatching actor');
}
// Already registered with this server?
const note = await Notes.findOneBy({ uri });
if (note == null) {
return await createNote(post, resolver);
}
if (note.userId !== actor.id) {
throw new Error('Refusing to ingest update for note of different user');
}
// Whether to tell clients the note has been updated and requires refresh.
let updating = false;
@ -699,6 +724,10 @@ export async function updateNote(value: string | IObject, resolver?: Resolver) {
if (poll) {
const dbPoll = await Polls.findOneBy({ noteId: note.id });
if (poll?.votes != null && poll.votes.find(p => !Number.isInteger(p) || p < 0) !== undefined) {
throw new Error('Refusing to ingest poll with non-integer or negative vote count');
}
if (dbPoll == null) {
await Polls.insert({
noteId: note.id,

View file

@ -21,7 +21,7 @@ import { genId } from "@/misc/gen-id.js";
import { instanceChart, usersChart } from "@/services/chart/index.js";
import { UserPublickey } from "@/models/entities/user-publickey.js";
import { isDuplicateKeyValueError } from "@/misc/is-duplicate-key-value-error.js";
import { toPuny } from "@/misc/convert-host.js";
import { extractDbHost, toPuny } from "@/misc/convert-host.js";
import { UserProfile } from "@/models/entities/user-profile.js";
import { toArray } from "@/prelude/array.js";
import { fetchInstanceMetadata } from "@/services/fetch-instance-metadata.js";
@ -69,7 +69,7 @@ const summaryLength = 2048;
* @param uri Fetch target URI
*/
function validateActor(x: IObject, uri: string): IActor {
const expectHost = toPuny(new URL(uri).hostname);
const expectHost = extractDbHost(uri);
if (x == null) {
throw new Error("invalid Actor: object is null");
@ -83,10 +83,36 @@ function validateActor(x: IObject, uri: string): IActor {
throw new Error("invalid Actor: wrong id");
}
if (!(typeof x.inbox === "string" && x.inbox.length > 0)) {
if (!(typeof x.inbox === "string" && x.inbox.length > 0 && extractDbHost(x.inbox) === expectHost)) {
throw new Error("invalid Actor: wrong inbox");
}
if (!(typeof x.outbox === "string" && x.outbox.length > 0 && extractDbHost(getApId(x.outbox)) === expectHost)) {
throw new Error("invalid Actor: wrong outbox");
}
const sharedInboxObject = x.sharedInbox ?? (x.endpoints ? x.endpoints.sharedInbox : undefined);
if (sharedInboxObject != null) {
const sharedInbox = getApId(sharedInboxObject);
if (!(typeof sharedInbox === "string" && sharedInbox.length > 0 && extractDbHost(sharedInbox) === expectHost)) {
throw new Error("invalid Actor: wrong shared inbox");
}
}
if (x.followers != null) {
x.followers = getApId(x.followers);
if (!(typeof x.followers === "string" && x.followers.length > 0 && extractDbHost(x.followers) === expectHost)) {
throw new Error("invalid Actor: wrong followers");
}
}
if (x.following != null) {
x.following = getApId(x.following);
if (!(typeof x.following === "string" && x.following.length > 0 && extractDbHost(x.following) === expectHost)) {
throw new Error("invalid Actor: wrong following");
}
}
if (
!(
typeof x.preferredUsername === "string" &&
@ -114,7 +140,7 @@ function validateActor(x: IObject, uri: string): IActor {
x.summary = truncate(x.summary, summaryLength);
}
const idHost = toPuny(new URL(x.id!).hostname);
const idHost = toPuny(new URL(x.id!).host);
if (idHost !== expectHost) {
throw new Error("invalid Actor: id has different host");
}
@ -124,7 +150,7 @@ function validateActor(x: IObject, uri: string): IActor {
throw new Error("invalid Actor: publicKey.id is not a string");
}
const publicKeyIdHost = toPuny(new URL(x.publicKey.id).hostname);
const publicKeyIdHost = toPuny(new URL(x.publicKey.id).host);
if (publicKeyIdHost !== expectHost) {
throw new Error("invalid Actor: publicKey.id has different host");
}
@ -148,7 +174,7 @@ export async function fetchPerson(
if (cached) return cached;
// Fetch from the database if the URI points to this server
if (uri.startsWith(`${config.url}/`)) {
if (extractDbHost(uri) === toPuny(config.host)) {
const id = uri.split("/").pop();
const u = await Users.findOneBy({ id });
if (u) await uriPersonCache.set(uri, u);
@ -178,7 +204,7 @@ export async function createPerson(
): Promise<User> {
if (typeof uri !== "string") throw new Error("uri is not string");
if (uri.startsWith(config.url)) {
if (extractDbHost(uri) === toPuny(config.host)) {
throw new StatusError(
"cannot resolve local user",
400,
@ -195,10 +221,10 @@ export async function createPerson(
person = validateActor(object, uri);
}
catch (e: any) {
if (typeof object.publicKey?.owner !== 'string')
// Work around GoToSocial issue #1186 (ref: https://github.com/superseriousbusiness/gotosocial/issues/1186)
if (typeof object.publicKey?.owner !== 'string' || object.inbox != null)
throw e;
// Work around GoToSocial issue #1186 (ref: https://github.com/superseriousbusiness/gotosocial/issues/1186)
logger.info(`Received stub actor, re-resolving with key owner uri: ${object.publicKey.owner}`);
object = (await resolver.resolve(object.publicKey.owner)) as any;
person = validateActor(object, uri);
@ -261,12 +287,19 @@ export async function createPerson(
const bday = person["vcard:bday"]?.match(/^\d{4}-\d{2}-\d{2}/);
const url = getOneApHrefNullable(person.url);
let url = getOneApHrefNullable(person.url);
const urlUrl = url != null ? new URL(url) : null;
const uriUrl = new URL(uri);
if (url && !url.startsWith("https://")) {
if (urlUrl != null && urlUrl.protocol != 'https:') {
throw new Error(`unexpected schema of person url: ${url}`);
}
if (urlUrl != null && urlUrl.host != uriUrl.host) {
logger.debug("Person url host doesn't match person uri host, clearing variable");
url = undefined;
}
let followersCount: number | undefined;
if (typeof person.followers === "string") {
@ -474,7 +507,7 @@ export async function updatePerson(
if (typeof uri !== "string") throw new Error("uri is not string");
// Skip if the URI points to this server
if (uri.startsWith(`${config.url}/`)) {
if (extractDbHost(uri) === toPuny(config.host)) {
return;
}

View file

@ -5,6 +5,7 @@ import { getApId, isQuestion } from "../type.js";
import { apLogger } from "../logger.js";
import { Notes, Polls } from "@/models/index.js";
import type { IPoll } from "@/models/entities/poll.js";
import { extractDbHost, toPuny } from "@/misc/convert-host.js";
export async function extractPollFromQuestion(
source: string | IObject,
@ -55,7 +56,7 @@ export async function updateQuestion(
const uri = typeof value === "string" ? value : getApId(value);
// Skip if URI points to this server
if (uri.startsWith(`${config.url}/`)) throw new Error("uri points local");
if (extractDbHost(uri) === toPuny(config.host)) throw new Error("uri points local");
//#region Already registered with this server?
const note = await Notes.findOneBy({ uri });

View file

@ -51,7 +51,7 @@ function inbox(ctx: Router.RouterContext) {
let signature;
try {
signature = httpSignature.parseRequest(ctx.req, { headers: ['(request-target)', 'digest', 'host', 'date'] });
signature = httpSignature.parseRequest(ctx.req, { headers: ['(request-target)', 'digest', 'host', 'date'], authorizationHeaderName: 'signature' });
} catch (e) {
ctx.status = 401;
return;

View file

@ -6,6 +6,7 @@ export const meta = {
tags: ["federation"],
requireCredential: true,
requireAdmin: true,
limit: {
duration: HOUR,

View file

@ -51,40 +51,6 @@ export async function proxyMedia(ctx: Koa.Context) {
if (ctx.status == 429) return;
const { hostname } = new URL(url);
let resolvedIps;
try {
resolvedIps = await promises.resolve(hostname);
} catch (error) {
ctx.status = 400;
ctx.body = { message: "Invalid URL" };
return;
}
const isSSRF = resolvedIps.some((ip) => {
if (net.isIPv4(ip)) {
const parts = ip.split(".").map(Number);
return (
parts[0] === 10 ||
(parts[0] === 172 && parts[1] >= 16 && parts[1] < 32) ||
(parts[0] === 192 && parts[1] === 168) ||
parts[0] === 127 ||
parts[0] === 0
);
} else if (net.isIPv6(ip)) {
return (
ip.startsWith("::") || ip.startsWith("fc00:") || ip.startsWith("fe80:")
);
}
return false;
});
if (isSSRF) {
ctx.status = 400;
ctx.body = { message: "Access to this URL is not allowed" };
return;
}
// Create temp file
const [path, cleanup] = await createTemp();

View file

@ -169,7 +169,7 @@
{{ i18n.ts.updateRemoteUser }}</FormButton
>
<FormFolder class="_formBlock">
<FormFolder class="_formBlock" v-if="iAmAdmin">
<template #label>Raw</template>
<MkObjectView v-if="ap" tall :value="ap">
@ -577,13 +577,15 @@ watch(
},
);
watch($$(user), () => {
os.api("ap/get", {
uri: user.uri ?? `${url}/users/${user.id}`,
}).then((res) => {
ap = res;
if (iAmAdmin) {
watch($$(user), () => {
os.api("ap/get", {
uri: user.uri ?? `${url}/users/${user.id}`,
}).then((res) => {
ap = res;
});
});
});
}
const headerActions = $computed(() => []);

View file

@ -15,7 +15,7 @@
/>
<MkRemoteCaution
v-if="user.host != null"
:href="user.url"
:href="user.url ?? user.uri"
class="warn"
/>

View file

@ -293,7 +293,7 @@ export function getUserMenu(user, router: Router = mainRouter) {
type: "a",
icon: "ph-arrow-square-out ph-bold ph-lg",
text: i18n.ts.showOnRemote,
href: user.url,
href: user.url ?? user.uri,
target: "_blank",
}
: undefined,

View file

@ -5593,7 +5593,7 @@ __metadata:
mfm-js: "npm:0.23.3"
mime-types: "npm:2.1.35"
mocha: "npm:10.2.0"
msgpackr: "npm:1.9.5"
msgpackr: "npm:1.11.2"
multer: "npm:1.4.4-lts.1"
nested-property: "npm:4.0.0"
node-fetch: "npm:3.3.2"
@ -5615,7 +5615,7 @@ __metadata:
qs: "npm:6.11.2"
random-seed: "npm:0.3.0"
ratelimiter: "npm:3.4.1"
re2: "npm:^1.20.11"
re2: "npm:^1.21.4"
redis-lock: "npm:0.1.4"
redis-semaphore: "npm:5.3.1"
reflect-metadata: "npm:0.1.13"
@ -15035,15 +15035,15 @@ __metadata:
languageName: node
linkType: hard
"msgpackr@npm:1.9.5":
version: 1.9.5
resolution: "msgpackr@npm:1.9.5"
"msgpackr@npm:1.11.2":
version: 1.11.2
resolution: "msgpackr@npm:1.11.2"
dependencies:
msgpackr-extract: "npm:^3.0.2"
dependenciesMeta:
msgpackr-extract:
optional: true
checksum: 10/d95fbee39b6046bdee06c59af43efda7068c6d1b0406d82b345b2ffd31f917b58829f925ceb56bbd686374ae52c952d3106747c41097021a8218c023715c948a
checksum: 10/7602f1e91e5ba13f4289ec9cab0d3f3db87d4ed323bebcb40a0c43ba2f6153192bffb63a5bb4755faacb6e0985f307c35084f40eaba1c325b7035da91381f01a
languageName: node
linkType: hard
@ -15099,12 +15099,12 @@ __metadata:
languageName: node
linkType: hard
"nan@npm:^2.19.0":
version: 2.19.0
resolution: "nan@npm:2.19.0"
"nan@npm:^2.20.0":
version: 2.22.0
resolution: "nan@npm:2.22.0"
dependencies:
node-gyp: "npm:latest"
checksum: 10/b97f680753113bcd803cb174e40baa01e04aa4cb95ee62b48841336d9c48b278a2eeff71a4a0d7315b8f639fb1e38049925d3be1c6e266c158dc8f7d95d67eaa
checksum: 10/ab165ba910e549fcc21fd561a33f534d86e81ae36c97b1019dcfe506b09692ff867c97794a54b49c9a83b8b485f529f0f58d24966c3a11863c97dc70814f4d50
languageName: node
linkType: hard
@ -15277,9 +15277,9 @@ __metadata:
languageName: node
linkType: hard
"node-gyp@npm:^10.0.1":
version: 10.0.1
resolution: "node-gyp@npm:10.0.1"
"node-gyp@npm:^10.2.0":
version: 10.2.0
resolution: "node-gyp@npm:10.2.0"
dependencies:
env-paths: "npm:^2.2.0"
exponential-backoff: "npm:^3.1.1"
@ -15287,13 +15287,13 @@ __metadata:
graceful-fs: "npm:^4.2.6"
make-fetch-happen: "npm:^13.0.0"
nopt: "npm:^7.0.0"
proc-log: "npm:^3.0.0"
proc-log: "npm:^4.1.0"
semver: "npm:^7.3.5"
tar: "npm:^6.1.2"
tar: "npm:^6.2.1"
which: "npm:^4.0.0"
bin:
node-gyp: bin/node-gyp.js
checksum: 10/578cf0c821f258ce4b6ebce4461eca4c991a4df2dee163c0624f2fe09c7d6d37240be4942285a0048d307230248ee0b18382d6623b9a0136ce9533486deddfa8
checksum: 10/41773093b1275751dec942b985982fd4e7a69b88cae719b868babcef3880ee6168aaec8dcaa8cd0b9fa7c84873e36cc549c6cac6a124ee65ba4ce1f1cc108cfe
languageName: node
linkType: hard
@ -17073,10 +17073,10 @@ __metadata:
languageName: node
linkType: hard
"proc-log@npm:^3.0.0":
version: 3.0.0
resolution: "proc-log@npm:3.0.0"
checksum: 10/02b64e1b3919e63df06f836b98d3af002b5cd92655cab18b5746e37374bfb73e03b84fe305454614b34c25b485cc687a9eebdccf0242cda8fda2475dd2c97e02
"proc-log@npm:^4.1.0":
version: 4.2.0
resolution: "proc-log@npm:4.2.0"
checksum: 10/4e1394491b717f6c1ade15c570ecd4c2b681698474d3ae2d303c1e4b6ab9455bd5a81566211e82890d5a5ae9859718cc6954d5150bb18b09b72ecb297beae90a
languageName: node
linkType: hard
@ -17541,14 +17541,14 @@ __metadata:
languageName: node
linkType: hard
"re2@npm:^1.20.11":
version: 1.20.11
resolution: "re2@npm:1.20.11"
"re2@npm:^1.21.4":
version: 1.21.4
resolution: "re2@npm:1.21.4"
dependencies:
install-artifact-from-github: "npm:^1.3.5"
nan: "npm:^2.19.0"
node-gyp: "npm:^10.0.1"
checksum: 10/a8665c861c632c67db448832a5a6a0092a1a29b8b6b731d6ce10f0017ba2871620780a745a8b2cbdd77e57ecf9e7bc8983c7ec5e10e6da6c06079a98146db443
nan: "npm:^2.20.0"
node-gyp: "npm:^10.2.0"
checksum: 10/926871cc84dab656afc035118c79d121211f21f4154084d7f6c05a05f746b5355f04e80a773db9ca817718dde03c561421b0a962300698c9f2eeafa4f70fd364
languageName: node
linkType: hard
@ -19655,6 +19655,20 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:^6.2.1":
version: 6.2.1
resolution: "tar@npm:6.2.1"
dependencies:
chownr: "npm:^2.0.0"
fs-minipass: "npm:^2.0.0"
minipass: "npm:^5.0.0"
minizlib: "npm:^2.1.1"
mkdirp: "npm:^1.0.3"
yallist: "npm:^4.0.0"
checksum: 10/bfbfbb2861888077fc1130b84029cdc2721efb93d1d1fb80f22a7ac3a98ec6f8972f29e564103bbebf5e97be67ebc356d37fa48dbc4960600a1eb7230fbd1ea0
languageName: node
linkType: hard
"tdigest@npm:^0.1.1":
version: 0.1.2
resolution: "tdigest@npm:0.1.2"