You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
🆙 Security update Camera
- Add file size limits (2MB photos, 1MB thumbnails) with pre-read rejection - Add PNG dimension validation via IHDR header before full decode (prevents decompression bombs) - Double-check decoded dimensions against max 1024x1024 (photos) / 640x640 (thumbnails) - Increase render cooldown from 5s to 15s - Add daily render quota (50/day per user) with 24h rolling window - Fix cooldown message to show remaining seconds correctly - Add structured logging for all rejected uploads - Replace e.printStackTrace() with proper SLF4J logging
This commit is contained in:
+113
-6
@@ -10,15 +10,26 @@ import com.eu.habbo.messages.outgoing.camera.CameraURLComposer;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class CameraRoomPictureEvent extends MessageHandler {
|
||||
public static int CAMERA_RENDER_DELAY = 5;
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CameraRoomPictureEvent.class);
|
||||
|
||||
public static int CAMERA_RENDER_DELAY = 15;
|
||||
public static int MAX_IMAGE_BYTES = 2 * 1024 * 1024; // 2 MB max upload
|
||||
public static int MAX_IMAGE_WIDTH = 1024;
|
||||
public static int MAX_IMAGE_HEIGHT = 1024;
|
||||
public static int MAX_DAILY_RENDERS = 50;
|
||||
|
||||
private ByteBuf image = null;
|
||||
|
||||
@@ -47,7 +58,7 @@ public class CameraRoomPictureEvent extends MessageHandler {
|
||||
if (habboStats.cache.containsKey("camera_render_cooldown")) {
|
||||
int cameraTimestamp = (Integer) habboStats.cache.get("camera_render_cooldown");
|
||||
if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) {
|
||||
String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(timestamp - cameraTimestamp));
|
||||
String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(CAMERA_RENDER_DELAY - (timestamp - cameraTimestamp)));
|
||||
habbo.alert(alertMessage);
|
||||
if (habboInfo.getPhotoURL() != null) {
|
||||
String[] splittedPhotoURL = habboInfo.getPhotoURL().split("/");
|
||||
@@ -59,16 +70,55 @@ public class CameraRoomPictureEvent extends MessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Daily render quota check
|
||||
int dailyRenderCount = getDailyRenderCount(habboStats, timestamp);
|
||||
if (dailyRenderCount >= MAX_DAILY_RENDERS) {
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.daily.limit", "You have reached the daily photo limit. Try again tomorrow."));
|
||||
return;
|
||||
}
|
||||
incrementDailyRenderCount(habboStats, timestamp, dailyRenderCount);
|
||||
|
||||
habboStats.cache.put("camera_render_cooldown", timestamp);
|
||||
Room room = habboInfo.getCurrentRoom();
|
||||
if (room == null) return;
|
||||
|
||||
int count = this.packet.readInt();
|
||||
|
||||
// Reject oversized payloads before reading
|
||||
if (count <= 0 || count > MAX_IMAGE_BYTES) {
|
||||
LOGGER.warn("User {} attempted camera upload with invalid size: {} bytes", habboInfo.getUsername(), count);
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.image = this.packet.getBuffer().readBytes(count);
|
||||
if (this.image == null) return;
|
||||
|
||||
byte[] imageBytes = ByteBufUtil.getBytes(this.image, 0, 4, true);
|
||||
if (imageBytes == null || imageBytes.length < 4 || !isPNG(imageBytes)) return;
|
||||
if (imageBytes == null || imageBytes.length < 4 || !isPNG(imageBytes)) {
|
||||
LOGGER.warn("User {} attempted camera upload with non-PNG data", habboInfo.getUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate image dimensions before fully decoding
|
||||
int[] dimensions;
|
||||
try {
|
||||
dimensions = readPNGDimensions(this.image);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("User {} uploaded image with unreadable dimensions", habboInfo.getUsername());
|
||||
handleImageProcessingError(habbo);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimensions == null || dimensions[0] <= 0 || dimensions[1] <= 0
|
||||
|| dimensions[0] > MAX_IMAGE_WIDTH || dimensions[1] > MAX_IMAGE_HEIGHT) {
|
||||
LOGGER.warn("User {} attempted camera upload with invalid dimensions: {}x{}",
|
||||
habboInfo.getUsername(),
|
||||
dimensions != null ? dimensions[0] : "null",
|
||||
dimensions != null ? dimensions[1] : "null");
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedImage theImage;
|
||||
try (ByteBufInputStream in = new ByteBufInputStream(this.image)) {
|
||||
@@ -78,6 +128,19 @@ public class CameraRoomPictureEvent extends MessageHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theImage == null) {
|
||||
LOGGER.warn("User {} uploaded image that could not be decoded", habboInfo.getUsername());
|
||||
handleImageProcessingError(habbo);
|
||||
return;
|
||||
}
|
||||
|
||||
// Double-check decoded dimensions match expectations
|
||||
if (theImage.getWidth() > MAX_IMAGE_WIDTH || theImage.getHeight() > MAX_IMAGE_HEIGHT) {
|
||||
LOGGER.warn("User {} decoded image exceeds dimension limits: {}x{}", habboInfo.getUsername(), theImage.getWidth(), theImage.getHeight());
|
||||
handleImageProcessingError(habbo);
|
||||
return;
|
||||
}
|
||||
|
||||
String fileName = habboInfo.getId() + "_" + timestamp;
|
||||
String URL = fileName + ".png";
|
||||
String URLsmall = fileName + "_small.png";
|
||||
@@ -96,10 +159,12 @@ public class CameraRoomPictureEvent extends MessageHandler {
|
||||
|
||||
try {
|
||||
ImageIO.write(theImage, "png", imageFile);
|
||||
Image smallImage = theImage.getScaledInstance(theImage.getWidth(null) / 2, theImage.getHeight(null) / 2, Image.SCALE_SMOOTH);
|
||||
BufferedImage bi = new BufferedImage(smallImage.getWidth(null), smallImage.getHeight(null), BufferedImage.TYPE_INT_ARGB);
|
||||
int smallWidth = theImage.getWidth(null) / 2;
|
||||
int smallHeight = theImage.getHeight(null) / 2;
|
||||
BufferedImage bi = new BufferedImage(smallWidth, smallHeight, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D graphics2D = bi.createGraphics();
|
||||
graphics2D.drawImage(smallImage, 0, 0, null);
|
||||
graphics2D.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
|
||||
graphics2D.drawImage(theImage, 0, 0, smallWidth, smallHeight, null);
|
||||
graphics2D.dispose();
|
||||
ImageIO.write(bi, "png", smallImageFile);
|
||||
} catch (IOException e) {
|
||||
@@ -114,6 +179,48 @@ public class CameraRoomPictureEvent extends MessageHandler {
|
||||
return bytes[0] == (byte) 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read PNG dimensions from the IHDR chunk without fully decoding the image.
|
||||
* This prevents decompression bomb attacks by checking dimensions before allocation.
|
||||
*/
|
||||
private int[] readPNGDimensions(ByteBuf buf) throws IOException {
|
||||
try (ByteBufInputStream in = new ByteBufInputStream(buf.duplicate())) {
|
||||
try (ImageInputStream iis = ImageIO.createImageInputStream(in)) {
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
|
||||
if (!readers.hasNext()) return null;
|
||||
|
||||
ImageReader reader = readers.next();
|
||||
try {
|
||||
reader.setInput(iis);
|
||||
int width = reader.getWidth(0);
|
||||
int height = reader.getHeight(0);
|
||||
return new int[]{width, height};
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getDailyRenderCount(HabboStats stats, int currentTimestamp) {
|
||||
if (!stats.cache.containsKey("camera_daily_count") || !stats.cache.containsKey("camera_daily_reset")) {
|
||||
return 0;
|
||||
}
|
||||
int resetTimestamp = (Integer) stats.cache.get("camera_daily_reset");
|
||||
// Reset counter if more than 24 hours have passed
|
||||
if (currentTimestamp - resetTimestamp >= 86400) {
|
||||
return 0;
|
||||
}
|
||||
return (Integer) stats.cache.get("camera_daily_count");
|
||||
}
|
||||
|
||||
private void incrementDailyRenderCount(HabboStats stats, int currentTimestamp, int currentCount) {
|
||||
if (currentCount == 0) {
|
||||
stats.cache.put("camera_daily_reset", currentTimestamp);
|
||||
}
|
||||
stats.cache.put("camera_daily_count", currentCount + 1);
|
||||
}
|
||||
|
||||
private void handleImageProcessingError(Habbo habbo) {
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
}
|
||||
|
||||
+82
-5
@@ -10,14 +10,24 @@ import com.eu.habbo.messages.outgoing.camera.CameraRoomThumbnailSavedComposer;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
import io.netty.buffer.ByteBufUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
public class CameraRoomThumbnailEvent extends MessageHandler {
|
||||
public static int CAMERA_RENDER_DELAY = 5;
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CameraRoomThumbnailEvent.class);
|
||||
|
||||
public static int CAMERA_RENDER_DELAY = 15;
|
||||
public static int MAX_THUMBNAIL_BYTES = 1024 * 1024; // 1 MB max for thumbnails
|
||||
public static int MAX_THUMBNAIL_WIDTH = 640;
|
||||
public static int MAX_THUMBNAIL_HEIGHT = 640;
|
||||
|
||||
private ByteBuf image = null;
|
||||
|
||||
@@ -45,7 +55,7 @@ public class CameraRoomThumbnailEvent extends MessageHandler {
|
||||
if (habboStats.cache.containsKey("camera_render_cooldown")) {
|
||||
int cameraTimestamp = (Integer) habboStats.cache.get("camera_render_cooldown");
|
||||
if (timestamp - cameraTimestamp < CAMERA_RENDER_DELAY) {
|
||||
String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(timestamp - cameraTimestamp));
|
||||
String alertMessage = Emulator.getTexts().getValue("camera.wait").replace("%seconds%", Integer.toString(CAMERA_RENDER_DELAY - (timestamp - cameraTimestamp)));
|
||||
habbo.alert(alertMessage);
|
||||
return;
|
||||
}
|
||||
@@ -57,14 +67,58 @@ public class CameraRoomThumbnailEvent extends MessageHandler {
|
||||
if (room == null || !room.isOwner(habbo)) return;
|
||||
|
||||
int count = this.packet.readInt();
|
||||
|
||||
// Reject oversized payloads before reading
|
||||
if (count <= 0 || count > MAX_THUMBNAIL_BYTES) {
|
||||
LOGGER.warn("User {} attempted thumbnail upload with invalid size: {} bytes", habboInfo.getUsername(), count);
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.image = this.packet.getBuffer().readBytes(count);
|
||||
if (this.image == null || !isValidImage(this.image)) return;
|
||||
if (this.image == null || !isValidImage(this.image)) {
|
||||
LOGGER.warn("User {} attempted thumbnail upload with non-PNG data", habboInfo.getUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate dimensions before fully decoding (prevents decompression bombs)
|
||||
int[] dimensions;
|
||||
try {
|
||||
dimensions = readPNGDimensions(this.image);
|
||||
} catch (IOException e) {
|
||||
LOGGER.warn("User {} uploaded thumbnail with unreadable dimensions", habboInfo.getUsername());
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimensions == null || dimensions[0] <= 0 || dimensions[1] <= 0
|
||||
|| dimensions[0] > MAX_THUMBNAIL_WIDTH || dimensions[1] > MAX_THUMBNAIL_HEIGHT) {
|
||||
LOGGER.warn("User {} attempted thumbnail upload with invalid dimensions: {}x{}",
|
||||
habboInfo.getUsername(),
|
||||
dimensions != null ? dimensions[0] : "null",
|
||||
dimensions != null ? dimensions[1] : "null");
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
|
||||
BufferedImage theImage;
|
||||
try (ByteBufInputStream in = new ByteBufInputStream(this.image)) {
|
||||
theImage = ImageIO.read(in);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("Failed to decode thumbnail from user {}", habboInfo.getUsername(), e);
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (theImage == null) {
|
||||
LOGGER.warn("User {} uploaded thumbnail that could not be decoded", habboInfo.getUsername());
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Double-check decoded dimensions
|
||||
if (theImage.getWidth() > MAX_THUMBNAIL_WIDTH || theImage.getHeight() > MAX_THUMBNAIL_HEIGHT) {
|
||||
LOGGER.warn("User {} decoded thumbnail exceeds dimension limits: {}x{}", habboInfo.getUsername(), theImage.getWidth(), theImage.getHeight());
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
@@ -73,7 +127,7 @@ public class CameraRoomThumbnailEvent extends MessageHandler {
|
||||
try {
|
||||
ImageIO.write(theImage, "png", imageFile);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("Failed to write thumbnail for room {}", room.getId(), e);
|
||||
habbo.alert(Emulator.getTexts().getValue("camera.error.creation"));
|
||||
return;
|
||||
}
|
||||
@@ -87,4 +141,27 @@ public class CameraRoomThumbnailEvent extends MessageHandler {
|
||||
&& imageBytes[0] == (byte) 0x89 && imageBytes[1] == 0x50
|
||||
&& imageBytes[2] == 0x4E && imageBytes[3] == 0x47;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read PNG dimensions from the IHDR chunk without fully decoding the image.
|
||||
* This prevents decompression bomb attacks by checking dimensions before allocation.
|
||||
*/
|
||||
private int[] readPNGDimensions(ByteBuf buf) throws IOException {
|
||||
try (ByteBufInputStream in = new ByteBufInputStream(buf.duplicate())) {
|
||||
try (ImageInputStream iis = ImageIO.createImageInputStream(in)) {
|
||||
Iterator<ImageReader> readers = ImageIO.getImageReaders(iis);
|
||||
if (!readers.hasNext()) return null;
|
||||
|
||||
ImageReader reader = readers.next();
|
||||
try {
|
||||
reader.setInput(iis);
|
||||
int width = reader.getWidth(0);
|
||||
int height = reader.getHeight(0);
|
||||
return new int[]{width, height};
|
||||
} finally {
|
||||
reader.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user