Zion Boggan zionboggan.com ↗

Security hardening: parser overflow, non-root container, declared DNS dep (#8)

Audit pass over the protocol core. Three fixes:

- oversight-container: read_exact() used unchecked `*at + n` for the
  bounds check and slice. On 32-bit targets (armv7 Android), an
  attacker-controlled u32 length field could wrap the offset, defeat the
  check, and panic on the slice - a crash/DoS reachable from a malicious
  bundle on mobile. Switched to checked_add; a wrap is treated as
  truncation. Behavior is unchanged for valid input; cross-language
  conformance and all 17 container tests still pass.

- Dockerfile: the registry ran as root. Added an unprivileged `oversight`
  user (uid 1000) owning /data and /app so a registry RCE lands unprivileged.

- pyproject: oversight_dns imports dnslib but it was undeclared. Added a
  `dns` optional-dependency group and folded it into `all`.
942bd14   Z committed on May 30, 2026 (3 weeks ago)
Dockerfile +8 -0
@@ -19,6 +19,14 @@ VOLUME ["/data"]
ENV OVERSIGHT_DB=/data/oversight-registry.sqlite
ENV OVERSIGHT_DATA=/data
+# Run as an unprivileged user. /data is created and owned by the runtime user so
+# the volume is writable without root. A registry RCE then lands as uid 1000,
+# not root inside the container.
+RUN useradd --system --uid 1000 --create-home oversight \
+ && mkdir -p /data \
+ && chown -R oversight:oversight /data /app
+USER oversight
+
EXPOSE 8765
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
oversight-rust/oversight-container/src/lib.rs +18 -3
@@ -107,15 +107,30 @@ fn read_exact<'a>(
n: usize,
field: &'static str,
) -> Result<&'a [u8], ContainerError> {
- if buf.len() < *at + n {
+ // Use checked arithmetic for the end offset. On 32-bit targets (e.g. armv7
+ // Android) `*at + n` can wrap when `n` comes from an attacker-controlled
+ // u32 length field, defeating the bounds check and panicking on the slice
+ // below. A wrap is, by definition, past the end of the buffer, so treat it
+ // as truncation.
+ let end = match at.checked_add(n) {
+ Some(end) => end,
+ None => {
+ return Err(ContainerError::Truncated {
+ wanted: n,
+ got: buf.len().saturating_sub(*at),
+ field,
+ });
+ }
+ };
+ if buf.len() < end {
return Err(ContainerError::Truncated {
wanted: n,
got: buf.len().saturating_sub(*at),
field,
});
}
- let slice = &buf[*at..*at + n];
- *at += n;
+ let slice = &buf[*at..end];
+ *at = end;
Ok(slice)
}
pyproject.toml +4 -1
@@ -49,7 +49,10 @@ formats = [
"python-docx>=1.1.0",
"imagehash>=4.3.1",
]
-all = ["oversight-protocol[registry,formats]"]
+dns = [
+ "dnslib>=0.9.25",
+]
+all = ["oversight-protocol[registry,formats,dns]"]
[project.scripts]
oversight = "cli.oversight_rich:main"