From 5cfadfd7ace210f99fe26ffcff3664cba20be93a Mon Sep 17 00:00:00 2001 From: azures04 Date: Sun, 28 Dec 2025 09:02:10 +0100 Subject: [PATCH] Add validation schemas and improve texture handling Introduces zod-based validation schemas for Minecraft and Mojang API endpoints. Refactors texture route to support hash-based file serving and removes the old static texture route. Updates database schema for player properties and adds an event to clean expired certificates. Improves ValidationError formatting, adjusts skin/cape URL construction, and adds SSRF protection for skin uploads. --- errors/ValidationError.js | 66 +++++++++++++++++-- modules/databaseGlobals.js | 14 +++- repositories/userRepository.js | 6 +- .../minecraft/profile/namechange.js | 2 +- .../minecraft/profile/skins/index.js | 6 ++ routes/textures/texture.js | 13 ---- routes/textures/texture/[hash].js | 27 ++++++++ .../minecraftservices/minecraft/profile.js | 13 ++++ .../minecraft/profile/capes/active.js | 28 ++++++++ .../minecraft/profile/lookup/bulk/byname.js | 18 +++++ .../profile/lookup/name/[username].js | 13 ++++ .../minecraft/profile/name/[name].js | 22 +++++++ .../profile/name/[name]/available.js | 21 ++++++ .../minecraft/profile/namechange.js | 13 ++++ .../minecraft/profile/skins.js | 24 +++++++ .../minecraft/profile/skins/active.js | 13 ++++ .../minecraftservices/player/attributes.js | 29 ++++++++ .../minecraftservices/player/certificates.js | 13 ++++ .../minecraftservices/privacy/blocklist.js | 13 ++++ .../privacy/blocklist/[uuid].js | 28 ++++++++ schemas/minecraftservices/privileges.js | 29 ++++++++ .../minecraft/profile/lookup/bulk/byname.js | 18 +++++ .../profile/lookup/name/[username].js | 13 ++++ schemas/mojangapi/profiles/minecraft.js | 18 +++++ .../mojangapi/user/profiles/[uuid]/names.js | 18 +++++ .../users/profiles/minecraft/[username].js | 13 ++++ .../session/minecraft/profile/[uuid].js | 16 +++++ services/sessionsService.js | 4 +- services/userService.js | 3 + 29 files changed, 490 insertions(+), 24 deletions(-) delete mode 100644 routes/textures/texture.js create mode 100644 routes/textures/texture/[hash].js create mode 100644 schemas/minecraftservices/minecraft/profile.js create mode 100644 schemas/minecraftservices/minecraft/profile/capes/active.js create mode 100644 schemas/minecraftservices/minecraft/profile/lookup/bulk/byname.js create mode 100644 schemas/minecraftservices/minecraft/profile/lookup/name/[username].js create mode 100644 schemas/minecraftservices/minecraft/profile/name/[name].js create mode 100644 schemas/minecraftservices/minecraft/profile/name/[name]/available.js create mode 100644 schemas/minecraftservices/minecraft/profile/namechange.js create mode 100644 schemas/minecraftservices/minecraft/profile/skins.js create mode 100644 schemas/minecraftservices/minecraft/profile/skins/active.js create mode 100644 schemas/minecraftservices/player/attributes.js create mode 100644 schemas/minecraftservices/player/certificates.js create mode 100644 schemas/minecraftservices/privacy/blocklist.js create mode 100644 schemas/minecraftservices/privacy/blocklist/[uuid].js create mode 100644 schemas/minecraftservices/privileges.js create mode 100644 schemas/mojangapi/minecraft/profile/lookup/bulk/byname.js create mode 100644 schemas/mojangapi/profile/lookup/name/[username].js create mode 100644 schemas/mojangapi/profiles/minecraft.js create mode 100644 schemas/mojangapi/user/profiles/[uuid]/names.js create mode 100644 schemas/mojangapi/users/profiles/minecraft/[username].js create mode 100644 schemas/sessionsserver/session/minecraft/profile/[uuid].js diff --git a/errors/ValidationError.js b/errors/ValidationError.js index ea104e3..7743b54 100644 --- a/errors/ValidationError.js +++ b/errors/ValidationError.js @@ -1,14 +1,52 @@ const DefaultError = require("./DefaultError") const YggdrasilError = require("./YggdrasilError") const SessionError = require("./SessionError") +const ServiceError = require("./ServiceError") const logger = require("../modules/logger") class ValidationError extends DefaultError { - constructor(zodResult, config = {}, context = {}) { - const formattedErrors = zodResult.error.issues.map(e => ({ - field: e.path.join("."), - message: e.message - })) + constructor(result, config = {}, context = {}) { + let formattedErrors = [] + if (result && result.error && Array.isArray(result.error.issues)) { + formattedErrors = result.error.issues.flatMap(issue => { + if (issue.code === "unrecognized_keys") { + return issue.keys.map(key => ({ + field: [...issue.path, key].join("."), + message: "Field not allowed" + })) + } + + if (issue.code === "invalid_union") { + return { + field: issue.path.join("."), + message: "Invalid input format (union mismatch)" + } + } + + return { + field: issue.path.join("."), + message: issue.message + } + }) + } + else if (result instanceof Error) { + formattedErrors = [{ + field: "global", + message: result.message + }] + } + else if (typeof result === "string") { + formattedErrors = [{ + field: "global", + message: result + }] + } + else { + formattedErrors = [{ + field: "unknown", + message: "Unknown validation error" + }] + } const message = config.message || "Validation failed" const statusCode = config.code || 400 @@ -54,11 +92,27 @@ class ValidationError extends DefaultError { return err.serialize() } - return { + if (this.config.errorFormat === "ServiceError") { + const err = new ServiceError( + this.code, + this.context.path || "", + this.config.errorName || "ValidationException", + this.message, + this.formattedErrors + ) + return err.serialize() + } + const response = { code: this.code, message: this.message, errors: this.formattedErrors } + + if (this.cause) { + response.cause = this.cause + } + + return response } } diff --git a/modules/databaseGlobals.js b/modules/databaseGlobals.js index 7de608e..0768aef 100644 --- a/modules/databaseGlobals.js +++ b/modules/databaseGlobals.js @@ -45,7 +45,8 @@ async function setupDatabase() { name VARCHAR(256) NOT NULL, value VARCHAR(512) NOT NULL, uuid VARCHAR(36) NOT NULL, - FOREIGN KEY (uuid) REFERENCES players(uuid) + UNIQUE KEY unique_property (uuid, name), + FOREIGN KEY (uuid) REFERENCES players(uuid) ON DELETE CASCADE ) `) logger.log(`${"playersProperties".bold} table ready`, ["MariaDB", "yellow"]) @@ -333,6 +334,17 @@ async function setupDatabase() { ) `) logger.log(`${"serverSessions".bold} table ready`, ["MariaDB", "yellow"]) + + await conn.query(`SET GLOBAL event_scheduler = ON;`) + logger.log("MariaDB Event Scheduler enabled.", ["MariaDB", "yellow"]) + + await conn.query(` + CREATE EVENT IF NOT EXISTS clean_expired_certificates + ON SCHEDULE EVERY 1 HOUR + DO + DELETE FROM playerCertificates WHERE expiresAt < NOW(); + `) + logger.log(`${"clean_expired_certificates".bold} event ready`, ["MariaDB", "yellow"]) logger.log("MariaDB database successfully initialised!", ["MariaDB", "yellow"]) diff --git a/repositories/userRepository.js b/repositories/userRepository.js index 97aafdc..e39f273 100644 --- a/repositories/userRepository.js +++ b/repositories/userRepository.js @@ -5,7 +5,11 @@ const { DefaultError } = require("../errors/errors") async function addPropertyToPlayer(key, value, uuid) { try { - const sql = `INSERT INTO playersProperties (name, value, uuid) VALUES (?, ?, ?)` + const sql = ` + INSERT INTO playersProperties (name, value, uuid) + VALUES (?, ?, ?) + ON DUPLICATE KEY UPDATE value = VALUES(value) + ` const result = await database.query(sql, [key, value, uuid]) if (result.affectedRows > 0) { diff --git a/routes/minecraftservices/minecraft/profile/namechange.js b/routes/minecraftservices/minecraft/profile/namechange.js index e50c196..f4578c0 100644 --- a/routes/minecraftservices/minecraft/profile/namechange.js +++ b/routes/minecraftservices/minecraft/profile/namechange.js @@ -3,7 +3,7 @@ const router = express.Router() const userService = require("../../../../services/userService") const authService = require("../../../../services/authService") -router.put("/", async (req, res) => { +router.get("/", async (req, res) => { const player = await authService.verifyAccessToken({ accessToken: req.headers.authorization.replace("Bearer ", "") }) const nameChangeInformation = await userService.getPlayerNameChangeStatus(player.user.uuid) return res.status(nameChangeInformation.code).json(nameChangeInformation.data) diff --git a/routes/minecraftservices/minecraft/profile/skins/index.js b/routes/minecraftservices/minecraft/profile/skins/index.js index 1bd03bc..e875bc6 100644 --- a/routes/minecraftservices/minecraft/profile/skins/index.js +++ b/routes/minecraftservices/minecraft/profile/skins/index.js @@ -1,3 +1,5 @@ +const fs = require("node:fs") +const path = require("node:path") const express = require("express") const router = express.Router() const multer = require("multer") @@ -22,7 +24,11 @@ const uploadLimiter = rateLimit({ max: 20, standardHeaders: true, legacyHeaders: false, + validate: { + ip: false + }, keyGenerator: (req) => { + rateLimit.ipKeyGenerator() return req.headers.authorization || req.ip }, handler: (req, res, next, options) => { diff --git a/routes/textures/texture.js b/routes/textures/texture.js deleted file mode 100644 index 060973c..0000000 --- a/routes/textures/texture.js +++ /dev/null @@ -1,13 +0,0 @@ -const express = require("express") -const router = express.Router() -const path = require("node:path") -const fs = require("node:fs") - -const TEXTURES_DIR = path.join(process.cwd(), "data", "textures") -if (!fs.existsSync(TEXTURES_DIR)) { - fs.mkdirSync(TEXTURES_DIR, { recursive: true }) -} - -router.use(express.static(TEXTURES_DIR)) - -module.exports = router \ No newline at end of file diff --git a/routes/textures/texture/[hash].js b/routes/textures/texture/[hash].js new file mode 100644 index 0000000..df9d29b --- /dev/null +++ b/routes/textures/texture/[hash].js @@ -0,0 +1,27 @@ +const express = require("express") +const router = express.Router({ mergeParams: true }) +const path = require("node:path") +const fs = require("node:fs") +const { DefaultError } = require("../../../errors/errors") + +const TEXTURES_DIR = path.join(process.cwd(), "data", "textures") + +router.get("/", async (req, res, next) => { + try { + const hash = req.params.hash + if (!/^[a-f0-9]{64}$/i.test(hash)) { + throw new DefaultError(404, "Texture not found") + } + + const subDir = hash.substring(0, 2) + const filePath = path.join(TEXTURES_DIR, subDir, hash) + if (!fs.existsSync(filePath)) { + throw new DefaultError(404, "Texture not found") + } + res.sendFile(filePath) + } catch (err) { + return next(err) + } +}) + +module.exports = router \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile.js b/schemas/minecraftservices/minecraft/profile.js new file mode 100644 index 0000000..655f12d --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile.js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + GET: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/capes/active.js b/schemas/minecraftservices/minecraft/profile/capes/active.js new file mode 100644 index 0000000..3161da4 --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/capes/active.js @@ -0,0 +1,28 @@ +const z = require("zod") + +module.exports = { + DELETE: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + }, + PUT: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }), + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + body: z.object({ + capeId: z.string().uuid({ message: "Invalid Cape UUID." }) + }), + error: { + code: 400, + message: "profile does not own cape", + error: "IllegalArgumentException" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/lookup/bulk/byname.js b/schemas/minecraftservices/minecraft/profile/lookup/bulk/byname.js new file mode 100644 index 0000000..3d14200 --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/lookup/bulk/byname.js @@ -0,0 +1,18 @@ +const z = require("zod") + +module.exports = { + POST: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }) + }), + body: z.array(z.string().trim().min(1)) + .min(1, { message: "RequestPayload is an empty array." }) + .max(10, { message: "RequestPayload has more than 10 elements." }), + error: { + code: 400, + error: "CONSTRAINT_VIOLATION", + errorMessage: "size must be between 1 and 10" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/lookup/name/[username].js b/schemas/minecraftservices/minecraft/profile/lookup/name/[username].js new file mode 100644 index 0000000..e1fca5c --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/lookup/name/[username].js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + GET: { + params: z.object({ + username: z.string().min(1) + }), + error: { + code: 404, + message: "Not Found" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/name/[name].js b/schemas/minecraftservices/minecraft/profile/name/[name].js new file mode 100644 index 0000000..dde4fa8 --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/name/[name].js @@ -0,0 +1,22 @@ +const z = require("zod") + +const nameSchema = z.string() + .min(1) + .max(16) + .regex(/^[a-zA-Z0-9_]+$/, { message: "Name can only contain alphanumeric characters and underscores." }); + +module.exports = { + PUT: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + params: z.object({ + name: nameSchema + }), + error: { + code: 400, + error: "CONSTRAINT_VIOLATION", + errorMessage: "Invalid profile name" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/name/[name]/available.js b/schemas/minecraftservices/minecraft/profile/name/[name]/available.js new file mode 100644 index 0000000..9515db1 --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/name/[name]/available.js @@ -0,0 +1,21 @@ +const z = require("zod") + +const nameSchema = z.string() + .min(1) + .max(16) + .regex(/^[a-zA-Z0-9_]+$/, { message: "Name can only contain alphanumeric characters and underscores." }); + +module.exports = { + GET: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + params: z.object({ + name: nameSchema + }), + error: { + code: 401, + message: "Unauthorized" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/namechange.js b/schemas/minecraftservices/minecraft/profile/namechange.js new file mode 100644 index 0000000..655f12d --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/namechange.js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + GET: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/skins.js b/schemas/minecraftservices/minecraft/profile/skins.js new file mode 100644 index 0000000..9ed9d94 --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/skins.js @@ -0,0 +1,24 @@ +const z = require("zod") + +module.exports = { + POST: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }), + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + body: z.object({ + variant: z.enum(["classic", "slim"], { + errorMap: () => ({ message: "Variant must be 'classic' or 'slim'." }) + }), + url: z.string() + .url({ message: "Invalid URL format." }) + .max(2048, { message: "URL is too long." }) + }), + error: { + code: 400, + message: "Invalid skin URL or variant.", + error: "IllegalArgumentException" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/minecraft/profile/skins/active.js b/schemas/minecraftservices/minecraft/profile/skins/active.js new file mode 100644 index 0000000..4f24f83 --- /dev/null +++ b/schemas/minecraftservices/minecraft/profile/skins/active.js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + DELETE: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/player/attributes.js b/schemas/minecraftservices/player/attributes.js new file mode 100644 index 0000000..b1b5f11 --- /dev/null +++ b/schemas/minecraftservices/player/attributes.js @@ -0,0 +1,29 @@ +const z = require("zod") + +module.exports = { + GET: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + }, + POST: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }), + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + body: z.object({ + profanityFilterPreferences: z.object({ + profanityFilterOn: z.boolean({ required_error: "profanityFilterOn is required" }) + }) + }), + error: { + code: 400, + message: "Invalid attributes format." + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/player/certificates.js b/schemas/minecraftservices/player/certificates.js new file mode 100644 index 0000000..3c3a827 --- /dev/null +++ b/schemas/minecraftservices/player/certificates.js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + POST: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/privacy/blocklist.js b/schemas/minecraftservices/privacy/blocklist.js new file mode 100644 index 0000000..655f12d --- /dev/null +++ b/schemas/minecraftservices/privacy/blocklist.js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + GET: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/privacy/blocklist/[uuid].js b/schemas/minecraftservices/privacy/blocklist/[uuid].js new file mode 100644 index 0000000..5e4cbe3 --- /dev/null +++ b/schemas/minecraftservices/privacy/blocklist/[uuid].js @@ -0,0 +1,28 @@ +const z = require("zod") + +module.exports = { + PUT: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + params: z.object({ + uuid: z.string().uuid({ message: "Invalid UUID." }) + }), + error: { + code: 400, + message: "Invalid UUID" + } + }, + DELETE: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + params: z.object({ + uuid: z.string().uuid({ message: "Invalid UUID." }) + }), + error: { + code: 400, + message: "Invalid UUID" + } + } +} \ No newline at end of file diff --git a/schemas/minecraftservices/privileges.js b/schemas/minecraftservices/privileges.js new file mode 100644 index 0000000..565226b --- /dev/null +++ b/schemas/minecraftservices/privileges.js @@ -0,0 +1,29 @@ +const z = require("zod") + +module.exports = { + GET: { + headers: z.object({ + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + error: { + code: 401, + message: "Unauthorized" + } + }, + POST: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }), + "authorization": z.string().min(1, { message: "Authorization header is required." }) + }), + body: z.object({ + profanityFilterPreferences: z.object({ + profanityFilterOn: z.boolean({ required_error: "profanityFilterOn is required" }) + }).optional() + }), + error: { + code: 401, + message: "Unauthorized" + } + } +} \ No newline at end of file diff --git a/schemas/mojangapi/minecraft/profile/lookup/bulk/byname.js b/schemas/mojangapi/minecraft/profile/lookup/bulk/byname.js new file mode 100644 index 0000000..3d14200 --- /dev/null +++ b/schemas/mojangapi/minecraft/profile/lookup/bulk/byname.js @@ -0,0 +1,18 @@ +const z = require("zod") + +module.exports = { + POST: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }) + }), + body: z.array(z.string().trim().min(1)) + .min(1, { message: "RequestPayload is an empty array." }) + .max(10, { message: "RequestPayload has more than 10 elements." }), + error: { + code: 400, + error: "CONSTRAINT_VIOLATION", + errorMessage: "size must be between 1 and 10" + } + } +} \ No newline at end of file diff --git a/schemas/mojangapi/profile/lookup/name/[username].js b/schemas/mojangapi/profile/lookup/name/[username].js new file mode 100644 index 0000000..de0b261 --- /dev/null +++ b/schemas/mojangapi/profile/lookup/name/[username].js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + GET: { + params: z.object({ + username: z.string().min(1, { message: "Username is required." }) + }), + error: { + code: 404, + message: "Not Found" + } + } +} \ No newline at end of file diff --git a/schemas/mojangapi/profiles/minecraft.js b/schemas/mojangapi/profiles/minecraft.js new file mode 100644 index 0000000..3d14200 --- /dev/null +++ b/schemas/mojangapi/profiles/minecraft.js @@ -0,0 +1,18 @@ +const z = require("zod") + +module.exports = { + POST: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }) + }), + body: z.array(z.string().trim().min(1)) + .min(1, { message: "RequestPayload is an empty array." }) + .max(10, { message: "RequestPayload has more than 10 elements." }), + error: { + code: 400, + error: "CONSTRAINT_VIOLATION", + errorMessage: "size must be between 1 and 10" + } + } +} \ No newline at end of file diff --git a/schemas/mojangapi/user/profiles/[uuid]/names.js b/schemas/mojangapi/user/profiles/[uuid]/names.js new file mode 100644 index 0000000..169d871 --- /dev/null +++ b/schemas/mojangapi/user/profiles/[uuid]/names.js @@ -0,0 +1,18 @@ +const z = require("zod") + +module.exports = { + GET: { + headers: z.object({ + "content-type": z.string() + .regex(/application\/json/i, { message: "Content-Type must be application/json" }) + .optional() + }), + params: z.object({ + uuid: z.string().uuid({ message: "Invalid UUID format." }) + }), + error: { + code: 204, + message: "No content" + } + } +} \ No newline at end of file diff --git a/schemas/mojangapi/users/profiles/minecraft/[username].js b/schemas/mojangapi/users/profiles/minecraft/[username].js new file mode 100644 index 0000000..de0b261 --- /dev/null +++ b/schemas/mojangapi/users/profiles/minecraft/[username].js @@ -0,0 +1,13 @@ +const z = require("zod") + +module.exports = { + GET: { + params: z.object({ + username: z.string().min(1, { message: "Username is required." }) + }), + error: { + code: 404, + message: "Not Found" + } + } +} \ No newline at end of file diff --git a/schemas/sessionsserver/session/minecraft/profile/[uuid].js b/schemas/sessionsserver/session/minecraft/profile/[uuid].js new file mode 100644 index 0000000..ca145b0 --- /dev/null +++ b/schemas/sessionsserver/session/minecraft/profile/[uuid].js @@ -0,0 +1,16 @@ +const z = require("zod") + +module.exports = { + GET: { + params: z.object({ + uuid: z.string().length(32).regex(/^[0-9a-fA-F]+$/, { message: "Invalid UUID (no dashes expected)." }) + }), + query: z.object({ + unsigned: z.enum(["true", "false"]).optional() + }), + error: { + code: 204, + message: "No content (UUID not found)" + } + } +} \ No newline at end of file diff --git a/services/sessionsService.js b/services/sessionsService.js index c250d5f..469a003 100644 --- a/services/sessionsService.js +++ b/services/sessionsService.js @@ -73,12 +73,12 @@ async function getProfile({ uuid, unsigned = false }) { const hasValidCape = !!activeCape const skinNode = hasValidSkin ? { - url: activeSkin.url, + url: (process.env.TEXTURES_ENDPOINTS || `http://localhost:${process.env.WEB_PORT}/textures/`) + activeSkin.url, metadata: activeSkin.variant === "SLIM" ? { model: "slim" } : undefined } : undefined const capeNode = hasValidCape ? { - url: activeCape.url + url: (process.env.TEXTURES_ENDPOINTS || `http://localhost:${process.env.WEB_PORT}/textures/`) + activeCape.url } : undefined const texturesObject = { diff --git a/services/userService.js b/services/userService.js index c9da2e9..bffe811 100644 --- a/services/userService.js +++ b/services/userService.js @@ -1,7 +1,9 @@ const fs = require("node:fs/promises") +const path = require("node:path") const util = require("node:util") const logger = require("../modules/logger") const crypto = require("node:crypto") +const ssrfcheck = require("ssrfcheck") const certsManager = require("../modules/certificatesManager") const userRepository = require("../repositories/userRepository") const { DefaultError } = require("../errors/errors") @@ -475,6 +477,7 @@ async function uploadSkin(uuid, fileObject, variant) { async function uploadSkinFromUrl(uuid, url, variant) { if (!url) throw new DefaultError(400, "Missing 'url' parameter.") + if (ssrfcheck.isSSRFSafeURL(url)) throw new DefaultError(400, "Bad request", null) let buffer try {