Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Scorecard supply-chain security
on:
branch_protection_rule:
schedule:
- cron: '30 1 * * 6'
push:
branches: [ "master" ]

permissions: read-all

jobs:
analysis:
name: Scorecard analysis
runs-on: ubuntu-latest
permissions:
security-events: write
id-token: write
steps:
- name: Checkout code
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: Run analysis
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
with:
results_file: results.sarif
results_format: sarif
publish_results: true

- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
retention-days: 5

- name: Upload to code-scanning
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
with:
sarif_file: results.sarif
51 changes: 51 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Test

on:
push:
branches: ['master', 'develop']
pull_request:

permissions:
contents: read

jobs:
hardening-guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- run: ./check-hardening.sh

build-and-smoke-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Build the image
run: docker build -t iip-test .
- name: Run smoke tests
run: ./test/smoke-test.sh iip-test
- name: Show container logs on failure
if: failure()
run: docker ps -a

static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Install cppcheck
run: sudo apt-get update && sudo apt-get install -y cppcheck
- name: Run cppcheck
# Informational only for now: this is a ~20 year old C/C++ codebase
# with no established cppcheck baseline, so treat findings as signal
# to review rather than a hard merge gate.
continue-on-error: true
run: |
cppcheck --enable=warning,portability --inconclusive --std=c++11 \
--suppress=missingInclude -j "$(nproc)" \
iipsrv/src 2> cppcheck-report.txt
cat cppcheck-report.txt
- name: Upload cppcheck report
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cppcheck-report
path: cppcheck-report.txt
retention-days: 5
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
FROM camicroscope/image-decoders:latest
# Pinned to the "latest" tag's digest as of this review (2026-07-30) for
# reproducible builds; re-resolve deliberately (docker buildx imagetools
# inspect camicroscope/image-decoders:latest) when a base image update is wanted.
FROM camicroscope/image-decoders@sha256:390a7c75ff991cfd81bb666bd5bcedcfc49106eb514acf0bf2adc7ed66bc26e2

### update
ARG DEBIAN_FRONTEND=noninteractive
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,34 @@ docker run iipsrv -d -p 4010:80
## usage
(include a directory of slides for iip)
http://localhost:4010/fcgi-bin/iipsrv.fcgi?DeepZoom=(path to slide)

## Security

iipsrv has **no authentication or authorization of its own** — it is a bare
FastCGI responder that trusts every request it receives. It must always run
behind a reverse proxy (or, as in caMicroscope Distro, behind the `back`
service) that authenticates callers before ever forwarding a request here.
**Never publish this service's port directly to a host or the public
internet.** In Distro, this is enforced by never giving the `iip` container a
`ports:` mapping; if you deploy this image yourself, don't add one either.

`FILESYSTEM_PREFIX` must be set to the directory containing your images (see
`fcgid.conf`). Since a security review on 2026-07-30, iipsrv refuses to start
if it's unset, since an empty prefix would otherwise let `FIF=`/IIIF requests
read any file the process has permission to open. See `SECURITY_REVIEW.md`
for the full review.

## Testing

There's no C++ unit test suite; instead `test/smoke-test.sh <image-tag>`
builds/runs the real container and checks it over HTTP (normal tile
requests, the `FILESYSTEM_PREFIX` jail, and the fail-closed startup check).
`./check-hardening.sh` guards against regressing on the fixes in
`SECURITY_REVIEW.md`. Both run in CI on every push/PR
(`.github/workflows/test.yml`).

```
docker build -t iip-test .
./test/smoke-test.sh iip-test
./check-hardening.sh
```
5 changes: 4 additions & 1 deletion apache2-iipsrv-fcgid.conf
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ FcgidInitialEnv LOGFILE "/tmp/iipsrv.log"
FcgidInitialEnv MAX_IMAGE_CACHE_SIZE "512"
FcgidInitialEnv JPEG_QUALITY "75"
FcgidInitialEnv MAX_CVT "5000"
#FcgidInitialEnv FILESYSTEM_PREFIX "/mnt/images/"
# Required: without this, iipsrv refuses to start (see Main.cc) rather than
# silently serving arbitrary files readable by the process. Must match the
# image directory mounted/copied into the container (see run.sh, docker-compose).
FcgidInitialEnv FILESYSTEM_PREFIX "/images/"
FcgidInitialEnv LD_LIBRARY_PATH "/usr/local/lib"
FcgidInitialEnv MAX_TILE_CACHE_SIZE "64"
FcgidInitialEnv BFBRIDGE_CACHEDIR "/tmp/"
Expand Down
55 changes: 55 additions & 0 deletions check-hardening.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env bash
#
# check-hardening.sh - guards against regressing on the security fixes made
# in SECURITY_REVIEW.md (2026-07-30). Run locally before pushing, or from CI.

set -uo pipefail

FAIL=0

# CVE-2021-46389 backport: dataLength must stay a size_t, not regress to int.
if ! grep -q 'size_t dataLength;' iipsrv/src/RawTile.h; then
echo "FAIL: iipsrv/src/RawTile.h :: dataLength is no longer size_t (CVE-2021-46389 regression)."
FAIL=1
fi

if ! grep -q 'if( !buffer )' iipsrv/src/TileManager.cc; then
echo "FAIL: iipsrv/src/TileManager.cc :: crop() no longer null-checks its malloc() result."
FAIL=1
fi

# FILESYSTEM_PREFIX must fail closed: Main.cc must still refuse to start unjailed.
if ! grep -q 'ALLOW_UNJAILED_FILESYSTEM' iipsrv/src/Main.cc; then
echo "FAIL: iipsrv/src/Main.cc :: the FILESYSTEM_PREFIX fail-closed check is missing."
FAIL=1
fi

# Shipped configs must ship FILESYSTEM_PREFIX uncommented and non-empty.
for conf in fcgid.conf apache2-iipsrv-fcgid.conf; do
if ! grep -qE '^\s*FcgidInitialEnv\s+FILESYSTEM_PREFIX\s+"[^"]+"' "$conf"; then
echo "FAIL: $conf :: FILESYSTEM_PREFIX is not set to a non-empty value (must not be commented out or empty)."
FAIL=1
fi
done

# Base image must be pinned by digest, not a mutable tag like ':latest'.
if grep -qE '^FROM\s+\S+:latest\s*$' Dockerfile; then
echo "FAIL: Dockerfile :: base image uses a mutable ':latest' tag instead of a pinned digest."
FAIL=1
fi
if ! grep -qE '^FROM\s+\S+@sha256:[0-9a-f]{64}' Dockerfile; then
echo "FAIL: Dockerfile :: base image is not pinned by digest (expected 'FROM ...@sha256:<digest>')."
FAIL=1
fi

# Compiler hardening flags must remain in configure.in.
if ! grep -q '_FORTIFY_SOURCE' iipsrv/configure.in; then
echo "FAIL: iipsrv/configure.in :: compiler hardening flags (_FORTIFY_SOURCE etc.) are missing."
FAIL=1
fi

if [[ $FAIL -eq 0 ]]; then
echo "OK: hardening checks passed."
fi

exit $FAIL
5 changes: 4 additions & 1 deletion fcgid.conf
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ FcgidInitialEnv LOGFILE "/tmp/iipsrv.log"
FcgidInitialEnv MAX_IMAGE_CACHE_SIZE "512"
FcgidInitialEnv JPEG_QUALITY "75"
FcgidInitialEnv MAX_CVT "5000"
#FcgidInitialEnv FILESYSTEM_PREFIX "/mnt/images/"
# Required: without this, iipsrv refuses to start (see Main.cc) rather than
# silently serving arbitrary files readable by the process. Must match the
# image directory mounted/copied into the container (see run.sh, docker-compose).
FcgidInitialEnv FILESYSTEM_PREFIX "/images/"
FcgidInitialEnv LD_LIBRARY_PATH "/usr/local/lib"
FcgidInitialEnv MAX_TILE_CACHE_SIZE "64"
FcgidInitialEnv CORS "*"
Expand Down
31 changes: 31 additions & 0 deletions iipsrv/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
compile
configure
depcomp
install-sh
libtool
ltmain.sh
missing
autom4te.cache
*.m4
*.Po
*.[oa]
*.fcgi
*~
Makefile
doc/html

# Files made by autogen.sh.
Makefile.in
libtool
/aclocal.m4
/autom4te.cache/
/compile
/config.*
/configure
/depcomp
/install-sh
/ltmain.sh
/m4/
/missing
/src/.deps/
fcgi/libfcgi/.deps/
8 changes: 8 additions & 0 deletions iipsrv/configure.in
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ AC_TYPE_SIZE_T
LT_INIT


# Security hardening flags
HARDENING_CFLAGS="-D_FORTIFY_SOURCE=2 -fstack-protector-strong -fPIE -Wformat -Wformat-security"
HARDENING_LDFLAGS="-pie -Wl,-z,relro,-z,now"
CFLAGS="$CFLAGS $HARDENING_CFLAGS"
CXXFLAGS="$CXXFLAGS $HARDENING_CFLAGS"
LDFLAGS="$LDFLAGS $HARDENING_LDFLAGS"


# Checks for header files.
AC_CHECK_HEADERS(glob.h)
AC_CHECK_HEADERS(time.h)
Expand Down
14 changes: 8 additions & 6 deletions iipsrv/src/KakaduImage.cc
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,12 @@ RawTilePtr KakaduImage::getTile( int seq, int ang, unsigned int res, int layers,


// Create our raw tile buffer and initialize some values
if( obpc == 16 ) rawtile->data = new unsigned short[tw*th*channels];
else if( obpc == 8 ) rawtile->data = new unsigned char[tw*th*channels];
size_t np = (size_t) tw * th * channels;
if( obpc == 16 ) rawtile->data = new unsigned short[np];
else if( obpc == 8 ) rawtile->data = new unsigned char[np];
else throw file_error( "Kakadu :: Unsupported number of bits" );

rawtile->dataLength = tw*th*channels*obpc/8;
rawtile->dataLength = np*obpc/8;
rawtile->filename = getImagePath();
rawtile->timestamp = timestamp;

Expand Down Expand Up @@ -369,11 +370,12 @@ RawTilePtr KakaduImage::getRegion( int seq, int ang, unsigned int res, int layer

RawTilePtr rawtile( 0, res, seq, ang, w, h, channels, obpc );

if( obpc == 16 ) rawtile->data = new unsigned short[w*h*channels];
else if( obpc == 8 ) rawtile->data = new unsigned char[w*h*channels];
size_t np = (size_t) w * h * channels;
if( obpc == 16 ) rawtile->data = new unsigned short[np];
else if( obpc == 8 ) rawtile->data = new unsigned char[np];
else throw file_error( "Kakadu :: Unsupported number of bits" );

rawtile->dataLength = w*h*channels*obpc/8;
rawtile->dataLength = np*obpc/8;
rawtile->filename = getImagePath();
rawtile->timestamp = timestamp;

Expand Down
13 changes: 13 additions & 0 deletions iipsrv/src/Main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,19 @@ int main( int argc, char *argv[] )
// Get the filesystem prefix if any
string filesystem_prefix = Environment::getFileSystemPrefix();

// Refuse to start unjailed: an empty FILESYSTEM_PREFIX means FIF/IIIF image
// paths are served relative to the filesystem root, so any caller who can
// reach this server can read arbitrary files it has permission to open.
// Require an explicit, deliberate opt-out to run without a prefix.
if( filesystem_prefix.empty() && !getenv( "ALLOW_UNJAILED_FILESYSTEM" ) ){
logfile << "FATAL :: FILESYSTEM_PREFIX is not set." << endl
<< "FATAL :: Refusing to start without a filesystem prefix, as this would allow"
<< " serving arbitrary files readable by this process (eg. FIF=/etc/passwd)." << endl
<< "FATAL :: Set FILESYSTEM_PREFIX to the directory containing your images, or set"
<< " ALLOW_UNJAILED_FILESYSTEM=true to explicitly opt out of this check." << endl << endl;
exit(1);
}


// Set up our watermark object
Watermark watermark( Environment::getWatermark(),
Expand Down
4 changes: 2 additions & 2 deletions iipsrv/src/RawTile.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class RawTile{
int memoryManaged;

/// The size of the data pointed to by data
int dataLength;
size_t dataLength;

/// The width in pixels of this tile
unsigned int width;
Expand Down Expand Up @@ -258,7 +258,7 @@ class RawTile{


/// Return the size of the data
int size() { return dataLength; }
size_t size() { return dataLength; }


/// Overloaded equality operator
Expand Down
Loading
Loading