Skip to content

Commit feac99b

Browse files
dfa1claude
andcommitted
fix(reader): sanitize HTTP Content-Range and blob slicing
fetchTail parsed the server-controlled Content-Range header with bare Long.parseLong and substring calls: a missing '-'/'/' or non-numeric field threw StringIndexOutOfBoundsException / NumberFormatException instead of a VortexException. fetchBlob sliced the tail with an unchecked length, throwing a raw IndexOutOfBoundsException. Parse defensively and slice via IoBounds so malformed remote responses surface as VortexException. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2df4e3a commit feac99b

1 file changed

Lines changed: 32 additions & 12 deletions

File tree

reader/src/main/java/io/github/dfa1/vortex/reader/VortexHttpReader.java

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -159,26 +159,46 @@ private static TailFetch fetchTail(URI uri, HttpClient client) throws IOExceptio
159159
// Content-Range: bytes <start>-<end>/<total>
160160
String cr = resp.headers().firstValue("Content-Range")
161161
.orElseThrow(() -> new VortexException("206 response missing Content-Range from " + uri));
162-
String spec = cr.substring("bytes ".length()); // "<start>-<end>/<total>"
162+
return parseContentRange(cr, body, uri);
163+
}
164+
165+
if (status == 200) {
166+
// Server returned full file (no Range support)
167+
return new TailFetch(body, 0L, body.length);
168+
}
169+
170+
throw new VortexException("HTTP " + status + " fetching tail of " + uri);
171+
}
172+
173+
/// Parses a `bytes <start>-<end>/<total>` Content-Range header from an untrusted server.
174+
/// Any structural defect (missing `bytes ` prefix, missing `-`/`/`, non-numeric fields)
175+
/// surfaces as a [VortexException] rather than a raw [NumberFormatException] or
176+
/// [StringIndexOutOfBoundsException].
177+
private static TailFetch parseContentRange(String contentRange, byte[] body, URI uri) {
178+
try {
179+
String prefix = "bytes ";
180+
if (!contentRange.startsWith(prefix)) {
181+
throw new VortexException("malformed Content-Range '" + contentRange + "' from " + uri);
182+
}
183+
String spec = contentRange.substring(prefix.length()); // "<start>-<end>/<total>"
184+
int dash = spec.indexOf('-');
163185
int slash = spec.indexOf('/');
186+
if (dash < 0 || slash < 0 || dash > slash) {
187+
throw new VortexException("malformed Content-Range '" + contentRange + "' from " + uri);
188+
}
189+
long start = Long.parseLong(spec.substring(0, dash));
190+
long end = Long.parseLong(spec.substring(dash + 1, slash));
164191
long total = Long.parseLong(spec.substring(slash + 1));
165-
long start = Long.parseLong(spec.substring(0, spec.indexOf('-')));
166-
long end = Long.parseLong(spec.substring(spec.indexOf('-') + 1, slash));
167192
long expected = end - start + 1;
168193
if (body.length != expected) {
169194
throw new VortexException(
170195
"HTTP tail from %s: Content-Range declares %d bytes but body has %d"
171196
.formatted(uri, expected, body.length));
172197
}
173198
return new TailFetch(body, start, total);
199+
} catch (NumberFormatException e) {
200+
throw new VortexException("malformed Content-Range '" + contentRange + "' from " + uri, e);
174201
}
175-
176-
if (status == 200) {
177-
// Server returned full file (no Range support)
178-
return new TailFetch(body, 0L, body.length);
179-
}
180-
181-
throw new VortexException("HTTP " + status + " fetching tail of " + uri);
182202
}
183203

184204
private static byte[] fetchRange(URI uri, long from, long to, HttpClient client) throws IOException {
@@ -215,8 +235,8 @@ private static MemorySegment fetchBlob(
215235
URI uri, HttpClient client
216236
) throws IOException {
217237
if (offset >= tailStart) {
218-
int relOffset = (int) (offset - tailStart);
219-
return MemorySegment.ofArray(tail).asSlice(relOffset, length);
238+
long relOffset = offset - tailStart;
239+
return IoBounds.slice(MemorySegment.ofArray(tail), relOffset, length);
220240
}
221241
byte[] bytes = fetchRange(uri, offset, offset + length - 1, client);
222242
return MemorySegment.ofArray(bytes);

0 commit comments

Comments
 (0)