Skip to content

Commit d1ff13e

Browse files
committed
Harden probe validation across parsers
tighten malformed-input handling in png, jpeg, heif, bmp, and tiff probes to reject spec-invalid fields and out-of-bounds metadata early. improve jpeg marker scanning alignment and heif fullbox/pixi parsing to avoid incorrect reads on corrupted payloads. standardize webp probe bit-depth reporting via a shared assumed constant and document current probe limitations in readme. bump project version and changelog for 1.1.3 release.
1 parent 2918f01 commit d1ff13e

File tree

9 files changed

+103
-22
lines changed

9 files changed

+103
-22
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.3] - 2026-03-16
9+
10+
### Fixed
11+
- Added strict PNG bit-depth validation in [`PngParser.parse()`](src/main/java/me/tamkungz/codecmedia/internal/image/png/PngParser.java) to reject malformed IHDR values outside the PNG spec (`1`, `2`, `4`, `8`, `16`).
12+
- Added helper [`PngParser.isValidBitDepth()`](src/main/java/me/tamkungz/codecmedia/internal/image/png/PngParser.java) to centralize allowed PNG bit-depth values during probe.
13+
- Added PNG color-type validation in [`PngParser.parse()`](src/main/java/me/tamkungz/codecmedia/internal/image/png/PngParser.java) with helper [`PngParser.isValidColorType()`](src/main/java/me/tamkungz/codecmedia/internal/image/png/PngParser.java) to accept only spec-valid values (`0`, `2`, `3`, `4`, `6`).
14+
- Refined JPEG signature sniffing in [`JpegParser.isLikelyJpeg()`](src/main/java/me/tamkungz/codecmedia/internal/image/jpeg/JpegParser.java) to require the exact 3-byte SOI/prefix check actually used by the parser.
15+
- Hardened JPEG SOF validation in [`JpegParser.parse()`](src/main/java/me/tamkungz/codecmedia/internal/image/jpeg/JpegParser.java) to reject invalid `bitsPerSample` (only `8`/`12`) and unsupported component counts (only `1`/`3`/`4`).
16+
- Improved JPEG marker traversal in [`JpegParser.parse()`](src/main/java/me/tamkungz/codecmedia/internal/image/jpeg/JpegParser.java) to correctly tolerate repeated `0xFF` fill bytes while preserving marker alignment validation.
17+
- Corrected HEIF `pixi` FullBox payload parsing in [`HeifParser.extractPixiBitDepth()`](src/main/java/me/tamkungz/codecmedia/internal/image/heif/HeifParser.java) by skipping the 4-byte FullBox header before reading channel count and per-channel depths.
18+
- Documented FullBox offset semantics for `ispe` extraction in [`HeifParser.extractIspeWidth()`](src/main/java/me/tamkungz/codecmedia/internal/image/heif/HeifParser.java) and [`HeifParser.extractIspeHeight()`](src/main/java/me/tamkungz/codecmedia/internal/image/heif/HeifParser.java).
19+
- Removed non-container boxes (`mdat`, `skip`, `free`) from recursive traversal candidates in [`HeifParser.isContainerType()`](src/main/java/me/tamkungz/codecmedia/internal/image/heif/HeifParser.java) to avoid unnecessary payload recursion.
20+
- Added bounds-checked big-endian integer reads in [`HeifParser.readBeInt()`](src/main/java/me/tamkungz/codecmedia/internal/image/heif/HeifParser.java) to return codec-domain errors instead of runtime index failures on malformed inputs.
21+
- Added strict BMP `bitsPerPixel` validation in [`BmpParser.parse()`](src/main/java/me/tamkungz/codecmedia/internal/image/bmp/BmpParser.java) via [`BmpParser.isValidBitsPerPixel()`](src/main/java/me/tamkungz/codecmedia/internal/image/bmp/BmpParser.java), allowing only spec-valid values (`1`, `2`, `4`, `8`, `16`, `24`, `32`).
22+
- Added defensive TIFF IFD entry-count bounds validation in [`TiffParser.parse()`](src/main/java/me/tamkungz/codecmedia/internal/image/tiff/TiffParser.java) to reject corrupt `entryCount` values that exceed available bytes.
23+
- Documented WebP probe bit-depth assumption in [`WebpParser`](src/main/java/me/tamkungz/codecmedia/internal/image/webp/WebpParser.java) with explicit constant [`ASSUMED_WEBP_BIT_DEPTH`](src/main/java/me/tamkungz/codecmedia/internal/image/webp/WebpParser.java), applied consistently across `VP8`, `VP8L`, and `VP8X` parsing.
24+
25+
### Verified
26+
- Confirmed compile stability after PNG/JPEG/HEIF/BMP/TIFF/WebP parser hardening with `mvn -q -DskipTests compile`.
27+
828
## [1.1.2] - 2026-03-15
929

1030
### Added

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# CodecMedia
22

33
[![MvnRepository](https://badges.mvnrepository.com/badge/me.tamkungz.codecmedia/codecmedia/badge.svg?label=MvnRepository)](https://mvnrepository.com/artifact/me.tamkungz.codecmedia/codecmedia)
4+
[![Sonatype Central](https://img.shields.io/badge/Sonatype%20Central-codecmedia-1f6feb)](https://central.sonatype.com/artifact/me.tamkungz.codecmedia/codecmedia)
45
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
56
[![Java](https://img.shields.io/badge/Java-17%2B-ED8B00?logo=openjdk&logoColor=white)](https://openjdk.org/)
67
[![Maven](https://img.shields.io/badge/Maven-3.9%2B-C71A36?logo=apachemaven&logoColor=white)](https://maven.apache.org/)
@@ -63,6 +64,8 @@ CodecMedia is a Java library for media probing, validation, metadata sidecar per
6364
- Audio-to-audio conversion is not implemented yet for real transcode cases (for example `mp3 -> ogg`).
6465
- The only temporary audio conversion path is a stub `wav <-> pcm` route.
6566
- Container/unknown conversion routes are intentionally unsupported unless explicitly mapped by the conversion route resolver.
67+
- TIFF probe currently reads the **first IFD/image** only (multi-page TIFF traversal is not implemented in probe mode).
68+
- WebP probe currently reports `bitDepth` as an assumed default (`8`) for `VP8`/`VP8L`/`VP8X` unless deeper profile metadata parsing is added.
6669
- For OpenAL workflows that require OGG from MP3 input, use an external transcoder first (for example ffmpeg), then play the produced OGG.
6770

6871
## Requirements

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
<groupId>me.tamkungz.codecmedia</groupId>
99
<artifactId>codecmedia</artifactId>
10-
<version>1.1.2</version>
10+
<version>1.1.3</version>
1111
<packaging>jar</packaging>
1212

1313
<name>CodecMedia</name>

src/main/java/me/tamkungz/codecmedia/internal/image/bmp/BmpParser.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,23 @@ public static BmpProbeInfo parse(byte[] bytes) throws CodecMediaException {
3939
if (width <= 0 || height <= 0) {
4040
throw new CodecMediaException("BMP has invalid dimensions");
4141
}
42-
if (bitsPerPixel <= 0) {
43-
throw new CodecMediaException("BMP has invalid bits-per-pixel");
42+
if (!isValidBitsPerPixel(bitsPerPixel)) {
43+
throw new CodecMediaException("BMP has invalid bits-per-pixel: " + bitsPerPixel);
4444
}
4545

4646
return new BmpProbeInfo(width, height, bitsPerPixel);
4747
}
4848

49+
private static boolean isValidBitsPerPixel(int bitsPerPixel) {
50+
return bitsPerPixel == 1
51+
|| bitsPerPixel == 2
52+
|| bitsPerPixel == 4
53+
|| bitsPerPixel == 8
54+
|| bitsPerPixel == 16
55+
|| bitsPerPixel == 24
56+
|| bitsPerPixel == 32;
57+
}
58+
4959
private static int readU16LE(byte[] bytes, int offset) throws CodecMediaException {
5060
if (offset + 2 > bytes.length) {
5161
throw new CodecMediaException("Unexpected end of BMP data");

src/main/java/me/tamkungz/codecmedia/internal/image/heif/HeifParser.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
public final class HeifParser {
88

9+
private static final int FULL_BOX_HEADER_SIZE = 4;
10+
911
private HeifParser() {
1012
}
1113

@@ -47,18 +49,20 @@ private static String readAscii(byte[] bytes, int offset, int length) {
4749
return new String(bytes, offset, length, StandardCharsets.US_ASCII);
4850
}
4951

50-
private static Integer extractIspeWidth(BoxData ispe) {
52+
private static Integer extractIspeWidth(BoxData ispe) throws CodecMediaException {
5153
if (ispe == null || ispe.payloadOffset() + 12 > ispe.boxEnd()) {
5254
return null;
5355
}
56+
// ispe is a FullBox: [version(1) + flags(3)] + width(4) + height(4)
5457
int width = readBeInt(ispe.bytes(), ispe.payloadOffset() + 4);
5558
return width > 0 ? width : null;
5659
}
5760

58-
private static Integer extractIspeHeight(BoxData ispe) {
61+
private static Integer extractIspeHeight(BoxData ispe) throws CodecMediaException {
5962
if (ispe == null || ispe.payloadOffset() + 12 > ispe.boxEnd()) {
6063
return null;
6164
}
65+
// ispe is a FullBox: [version(1) + flags(3)] + width(4) + height(4)
6266
int height = readBeInt(ispe.bytes(), ispe.payloadOffset() + 8);
6367
return height > 0 ? height : null;
6468
}
@@ -69,16 +73,17 @@ private static Integer extractPixiBitDepth(BoxData pixi) {
6973
}
7074
byte[] bytes = pixi.bytes();
7175
int payloadOffset = pixi.payloadOffset();
72-
if (payloadOffset + 1 > pixi.boxEnd()) {
76+
int dataOffset = payloadOffset + FULL_BOX_HEADER_SIZE;
77+
if (dataOffset + 1 > pixi.boxEnd()) {
7378
return null;
7479
}
75-
int channelCount = bytes[payloadOffset] & 0xFF;
76-
if (channelCount <= 0 || payloadOffset + 1 + channelCount > pixi.boxEnd()) {
80+
int channelCount = bytes[dataOffset] & 0xFF;
81+
if (channelCount <= 0 || dataOffset + 1 + channelCount > pixi.boxEnd()) {
7782
return null;
7883
}
7984
int minDepth = Integer.MAX_VALUE;
8085
for (int i = 0; i < channelCount; i++) {
81-
int depth = bytes[payloadOffset + 1 + i] & 0xFF;
86+
int depth = bytes[dataOffset + 1 + i] & 0xFF;
8287
if (depth > 0 && depth < minDepth) {
8388
minDepth = depth;
8489
}
@@ -204,9 +209,6 @@ private static boolean isContainerType(String type) {
204209
|| "ilst".equals(type)
205210
|| "tref".equals(type)
206211
|| "mfra".equals(type)
207-
|| "skip".equals(type)
208-
|| "free".equals(type)
209-
|| "mdat".equals(type)
210212
|| "jp2h".equals(type)
211213
|| "res ".equals(type)
212214
|| "uuid".equals(type)
@@ -244,7 +246,10 @@ private static long readU64AsLong(byte[] bytes, int offset) {
244246
| (bytes[offset + 7] & 0xFFL);
245247
}
246248

247-
private static int readBeInt(byte[] bytes, int offset) {
249+
private static int readBeInt(byte[] bytes, int offset) throws CodecMediaException {
250+
if (offset < 0 || offset + 4 > bytes.length) {
251+
throw new CodecMediaException("Unexpected end of HEIF data");
252+
}
248253
return ((bytes[offset] & 0xFF) << 24)
249254
| ((bytes[offset + 1] & 0xFF) << 16)
250255
| ((bytes[offset + 2] & 0xFF) << 8)

src/main/java/me/tamkungz/codecmedia/internal/image/jpeg/JpegParser.java

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ private JpegParser() {
88
}
99

1010
public static boolean isLikelyJpeg(byte[] bytes) {
11-
return bytes.length >= 4
11+
return bytes.length >= 3
1212
&& (bytes[0] & 0xFF) == 0xFF
1313
&& (bytes[1] & 0xFF) == 0xD8
1414
&& (bytes[2] & 0xFF) == 0xFF;
@@ -21,12 +21,19 @@ public static JpegProbeInfo parse(byte[] bytes) throws CodecMediaException {
2121

2222
int pos = 2; // after SOI
2323
while (pos + 4 <= bytes.length) {
24-
if ((bytes[pos] & 0xFF) != 0xFF) {
24+
int markerPrefixStart = pos;
25+
while (pos < bytes.length && (bytes[pos] & 0xFF) == 0xFF) {
26+
pos++; // skip marker prefix + fill bytes between markers
27+
}
28+
if (pos == markerPrefixStart) {
2529
throw new CodecMediaException("Invalid JPEG marker alignment");
2630
}
31+
if (pos >= bytes.length) {
32+
throw new CodecMediaException("Unexpected end of JPEG while reading marker");
33+
}
2734

28-
int marker = bytes[pos + 1] & 0xFF;
29-
pos += 2;
35+
int marker = bytes[pos] & 0xFF;
36+
pos++;
3037

3138
if (marker == 0xD9 || marker == 0xDA) {
3239
break; // EOI or SOS; no frame header found yet
@@ -60,8 +67,14 @@ public static JpegProbeInfo parse(byte[] bytes) throws CodecMediaException {
6067
int width = readBeShort(bytes, segmentDataStart + 3);
6168
int channels = bytes[segmentDataStart + 5] & 0xFF;
6269

63-
if (width <= 0 || height <= 0 || channels <= 0) {
64-
throw new CodecMediaException("JPEG has invalid dimensions/components");
70+
if (width <= 0 || height <= 0) {
71+
throw new CodecMediaException("JPEG has invalid dimensions");
72+
}
73+
if (!isValidBitsPerSample(bitsPerSample)) {
74+
throw new CodecMediaException("Invalid JPEG bit precision: " + bitsPerSample);
75+
}
76+
if (!isValidChannels(channels)) {
77+
throw new CodecMediaException("Invalid JPEG component count: " + channels);
6578
}
6679
return new JpegProbeInfo(width, height, bitsPerSample, channels);
6780
}
@@ -79,6 +92,14 @@ private static int readBeShort(byte[] bytes, int offset) throws CodecMediaExcept
7992
return ((bytes[offset] & 0xFF) << 8) | (bytes[offset + 1] & 0xFF);
8093
}
8194

95+
private static boolean isValidBitsPerSample(int bitsPerSample) {
96+
return bitsPerSample == 8 || bitsPerSample == 12;
97+
}
98+
99+
private static boolean isValidChannels(int channels) {
100+
return channels == 1 || channels == 3 || channels == 4;
101+
}
102+
82103
private static boolean isSofMarker(int marker) {
83104
return marker == 0xC0 || marker == 0xC1 || marker == 0xC2
84105
|| marker == 0xC3 || marker == 0xC5 || marker == 0xC6

src/main/java/me/tamkungz/codecmedia/internal/image/png/PngParser.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,24 @@ public static PngProbeInfo parse(byte[] bytes) throws CodecMediaException {
5050
if (width <= 0 || height <= 0) {
5151
throw new CodecMediaException("PNG has invalid dimensions");
5252
}
53+
if (!isValidBitDepth(bitDepth)) {
54+
throw new CodecMediaException("PNG has invalid bit depth: " + bitDepth);
55+
}
56+
if (!isValidColorType(colorType)) {
57+
throw new CodecMediaException("PNG has invalid color type: " + colorType);
58+
}
5359

5460
return new PngProbeInfo(width, height, bitDepth, colorType);
5561
}
5662

63+
private static boolean isValidBitDepth(int bitDepth) {
64+
return bitDepth == 1 || bitDepth == 2 || bitDepth == 4 || bitDepth == 8 || bitDepth == 16;
65+
}
66+
67+
private static boolean isValidColorType(int colorType) {
68+
return colorType == 0 || colorType == 2 || colorType == 3 || colorType == 4 || colorType == 6;
69+
}
70+
5771
private static int readBeInt(byte[] bytes, int offset) throws CodecMediaException {
5872
if (offset + 4 > bytes.length) {
5973
throw new CodecMediaException("Unexpected end of PNG data");

src/main/java/me/tamkungz/codecmedia/internal/image/tiff/TiffParser.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ public static TiffProbeInfo parse(byte[] bytes) throws CodecMediaException {
2626

2727
int entryCount = readU16(bytes, ifdOffset, littleEndian);
2828
int pos = ifdOffset + 2;
29+
int maxEntries = (bytes.length - pos) / 12;
30+
if (entryCount > maxEntries) {
31+
throw new CodecMediaException("TIFF IFD entry count exceeds available data");
32+
}
33+
2934
Integer width = null;
3035
Integer height = null;
3136
Integer bitDepth = null;

src/main/java/me/tamkungz/codecmedia/internal/image/webp/WebpParser.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
public final class WebpParser {
66

7+
// Probe-level default: WebP variants are reported as 8-bit unless deeper bit-depth metadata is parsed.
8+
private static final int ASSUMED_WEBP_BIT_DEPTH = 8;
9+
710
private WebpParser() {
811
}
912

@@ -36,7 +39,7 @@ private static WebpProbeInfo parseVp8X(byte[] bytes) throws CodecMediaException
3639
}
3740
int widthMinus1 = (bytes[24] & 0xFF) | ((bytes[25] & 0xFF) << 8) | ((bytes[26] & 0xFF) << 16);
3841
int heightMinus1 = (bytes[27] & 0xFF) | ((bytes[28] & 0xFF) << 8) | ((bytes[29] & 0xFF) << 16);
39-
return ensurePositive(widthMinus1 + 1, heightMinus1 + 1, 8, "VP8X");
42+
return ensurePositive(widthMinus1 + 1, heightMinus1 + 1, ASSUMED_WEBP_BIT_DEPTH, "VP8X");
4043
}
4144

4245
private static WebpProbeInfo parseVp8L(byte[] bytes) throws CodecMediaException {
@@ -52,7 +55,7 @@ private static WebpProbeInfo parseVp8L(byte[] bytes) throws CodecMediaException
5255
int b4 = bytes[24] & 0xFF;
5356
int widthMinus1 = b1 | ((b2 & 0x3F) << 8);
5457
int heightMinus1 = ((b2 >> 6) & 0x03) | (b3 << 2) | ((b4 & 0x0F) << 10);
55-
return ensurePositive(widthMinus1 + 1, heightMinus1 + 1, 8, "VP8L");
58+
return ensurePositive(widthMinus1 + 1, heightMinus1 + 1, ASSUMED_WEBP_BIT_DEPTH, "VP8L");
5659
}
5760

5861
private static WebpProbeInfo parseVp8(byte[] bytes) throws CodecMediaException {
@@ -64,7 +67,7 @@ private static WebpProbeInfo parseVp8(byte[] bytes) throws CodecMediaException {
6467
}
6568
int width = ((bytes[27] & 0x3F) << 8) | (bytes[26] & 0xFF);
6669
int height = ((bytes[29] & 0x3F) << 8) | (bytes[28] & 0xFF);
67-
return ensurePositive(width, height, 8, "VP8");
70+
return ensurePositive(width, height, ASSUMED_WEBP_BIT_DEPTH, "VP8");
6871
}
6972

7073
private static String fourcc(byte[] bytes, int offset) throws CodecMediaException {

0 commit comments

Comments
 (0)