diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ac344a2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# Keeps the SHA-pinned GitHub Actions up to date via reviewed PRs. +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + groups: + actions: + patterns: ["*"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a45084..1125b0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,13 +10,17 @@ on: type: boolean default: false +# Least privilege: jobs get read-only tokens unless they ask for more. +permissions: + contents: read + jobs: build: # Everything is built on Linux with open-source tools only: # GCC (tests), MinGW-w64 (Windows binaries), NSIS (installer). runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Install toolchain run: sudo apt-get update && sudo apt-get install -y build-essential gcc-mingw-w64-x86-64 nsis python3 - name: Native build and tests (-Werror) @@ -34,7 +38,7 @@ jobs: run: scripts/ci-run.sh "mingw build" make windows CFLAGS="-O2 -Werror" - name: Installer (NSIS) run: scripts/ci-run.sh "nsis" make installer CFLAGS="-O2 -Werror" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sp410-opensource-windows path: | @@ -50,12 +54,12 @@ jobs: needs: build runs-on: windows-latest steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: sp410-opensource-windows path: build - - uses: actions/setup-python@v5 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.12" - name: Test suite against the Windows binaries @@ -102,7 +106,7 @@ jobs: python -c "import sys; sys.path.insert(0,'tools'); import tspl_decode as t; s=t.parse(open(r'$($f.FullName)','rb').read()); l=s.labels[0]; print(l.width, l.height, l.black_dots()); assert (l.width, l.height) == (812, 1219) and l.black_dots() > 500, 'unexpected label'" $cmds = (Get-Content e2e-capture\commands.txt | Select-Object -First 14) -join "%0A" Write-Output "::notice title=TSPL from a Windows print job::$cmds" - - uses: actions/upload-artifact@v4 + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() with: name: windows-e2e-capture @@ -115,7 +119,7 @@ jobs: lint: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - run: sudo apt-get install -y shellcheck - run: shellcheck scripts/*.sh - run: python3 -m py_compile tools/*.py tests/*.py @@ -128,11 +132,13 @@ jobs: if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.publish_release) needs: [build, windows, lint] runs-on: ubuntu-24.04 + # Publishing waits for approval from the 'release' environment's reviewers. + environment: release permissions: contents: write steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: sp410-opensource-windows path: build diff --git a/CHANGELOG.md b/CHANGELOG.md index c6ececa..ad60087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,34 @@ All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses [Semantic Versioning](https://semver.org/). +## [Unreleased] + +### Security +- Fixed a heap buffer overflow in the HTTP server's chunked-request handling: + a crafted Transfer-Encoding: chunked body could overflow the body-length + accumulator and trigger an out-of-bounds write in the LocalSystem service + (reachable by any local user on 127.0.0.1, or remotely if shared). Chunk + sizes are now bounded before allocation. (audit V1) +- Added a configurable concurrent-connection cap ([server] max-connections, + default 64) so a connection flood or Slowloris cannot exhaust the service. + Excess connections get an immediate 503. (audit V2) +- The status page now HTML-escapes the client Host header, and all responses + carry Content-Security-Policy and X-Content-Type-Options: nosniff, removing + a reflected-XSS vector. (audit V3) +- The file: device scheme is now disabled in the service unless + [device] allow-file-device = true is set, so a tampered config cannot make + the SYSTEM service write to an arbitrary path. The CLI still allows file:. + (audit V4) +- Installer: %ProgramData%\SP410 is now ACL-restricted so only SYSTEM and + Administrators can modify sp410.ini (Users get read-only); editing settings + now requires elevation. (audit V4) +- The service logs a clear warning when bound to a non-loopback address + (unauthenticated network exposure). (audit V6) +- Tightened PWG raster geometry limits (<=10000x40000, <=64 Mpx/page) to bound + per-page allocations. (audit V11) +- Builds now use -fstack-protector-strong (ASLR/high-entropy/DEP already on; + Control Flow Guard still depends on the release toolchain). (audit V12) + ## [1.0.0] - 2026-10-04 First stable release. diff --git a/KEYS.asc b/KEYS.asc new file mode 100644 index 0000000..93b4ef1 --- /dev/null +++ b/KEYS.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEatnstxYJKwYBBAHaRw8BAQdA5jkbpyCr7uORe5nyQ8CBElCysDp67dWQQSkL +SquzWzC0KnJpZ2h0aXN3cm9uZyA8cmlnaHRpc3dyb25nQHByb3Rvbm1haWwuY29t +PoiQBBMWCAA4FiEEwkrHK6rYVS1QSMiQG/CrqGSTraMFAmrZ7LcCGwEFCwkIBwIG +FQoJCAsCBBYCAwECHgECF4AACgkQG/CrqGSTraOGrgD/eOVN/xjjTkcI642tsc7V +O90XgKgONgVOE9Q7myM39NwA/3a+BHZdIY8/olHWNcYgTylzOfDty1J8c0q4InQt +wAYJuDMEatntHBYJKwYBBAHaRw8BAQdAcq/22kkKh2AkMdmY8JwGjHkhbjR5VNIw +6qBlXkUkGOSI9QQYFggAJhYhBMJKxyuq2FUtUEjIkBvwq6hkk62jBQJq2e0cAhsC +BQkDwmcAAIEJEBvwq6hkk62jdiAEGRYIAB0WIQSTP0Qm3R7T6QPG+jdpZOBOHX4W +fQUCatntHAAKCRBpZOBOHX4WfSliAP9m6L6JThhly2xMhAwTVDMfLgdlyWqNSvUl +yvoL79WFxQEAjrA+I0IBjZ+Ox1UNWUmT6Yb2I1rT4Fb+Q7iSSvYuzQFpIAEA7sN3 +nmVw0wr19LbUjJFEOSIoNbOJYVNHSnIiPJsWjqMA/iRmzBr9BPKfcUD1SN/edRPl +hglXtf8smgr+ZZ9o8CkK +=Ms3e +-----END PGP PUBLIC KEY BLOCK----- diff --git a/Makefile b/Makefile index de65aa8..0326ce4 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,12 @@ endif WARN := -Wall -Wextra -Wshadow -Wformat=2 -Wstrict-prototypes \ -Wmissing-prototypes -Wno-unused-parameter CFLAGS ?= -O2 -g -ALL_CFLAGS := -std=c99 $(PLATFLAGS) -DSP410_VERSION=\"$(VERSION)\" $(WARN) $(CFLAGS) +# Exploit-mitigation hardening. Stack canaries are supported by both gcc and +# clang mingw; ASLR/high-entropy/DEP are on by default for these PE targets. +# Control Flow Guard needs clang/llvm-mingw (-mguard=cf + lld) and is left to +# the release toolchain; see docs. +HARDEN := -fstack-protector-strong +ALL_CFLAGS := -std=c99 $(PLATFLAGS) -DSP410_VERSION=\"$(VERSION)\" $(WARN) $(HARDEN) $(CFLAGS) CORE := src/core/dither.c src/core/tspl.c src/core/settings.c src/core/pwg.c \ src/core/render.c src/core/log.c diff --git a/SIGNING.md b/SIGNING.md new file mode 100644 index 0000000..0256ee0 --- /dev/null +++ b/SIGNING.md @@ -0,0 +1,119 @@ +# Verifying releases + +Every release from version 1.1.0 onwards ships two files alongside the +packages: + +| File | What it is | +|---|---| +| `SHA256SUMS.txt` | SHA-256 checksum of every file in the release | +| `SHA256SUMS.txt.asc` | The maintainer's detached OpenPGP signature over `SHA256SUMS.txt` | + +Verifying is a two-step chain: the **signature** proves `SHA256SUMS.txt` +came from the maintainer and hasn't been altered, and the **checksums** +prove your download matches it. Checking a checksum alone only proves the +file matches whatever list sat next to it on the same page, so do both. + +Releases before 1.1.0 (including 1.0.0) were not signed; only their +checksums are available. + +## The signing key + +| | | +|---|---| +| Owner | rightiswrong `` | +| Primary key fingerprint | `C24A C72B AAD8 552D 5048 C890 1BF0 ABA8 6493 ADA3` | +| Algorithm | ed25519 (signing subkey held on a hardware token) | + +The public key is in [`KEYS.asc`](KEYS.asc) in this repository and on +[keys.openpgp.org](https://keys.openpgp.org/). **Always compare the +fingerprint** that GnuPG prints with the one above — and, ideally, with one +obtained another way (e.g. the maintainer's GitHub profile) — before trusting +it. + +The signing subkey expires periodically and is extended by the maintainer; +if GnuPG says the key has expired, re-import a fresh copy of `KEYS.asc`. + +## One-time setup: import the key + +On Windows, install [Gpg4win](https://www.gpg4win.org/) (verify its own +download as described on that site). Then, in PowerShell: + +```powershell +Invoke-WebRequest -OutFile KEYS.asc https://raw.githubusercontent.com/rightiswrong/sp410-opensource-windows/main/KEYS.asc +gpg --import KEYS.asc +gpg --fingerprint rightiswrong@protonmail.com # compare with the fingerprint above +``` + +On Linux/macOS use the same `gpg` commands with `curl -fsSLO `. + +## Verifying a download + +Put the installer, `SHA256SUMS.txt` and `SHA256SUMS.txt.asc` in one folder +(e.g. `Downloads`), open PowerShell there, then: + +```powershell +# 1. Check the signature on the checksum list +gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt +``` + +You should see `Good signature from "rightiswrong "` +and a `Primary key fingerprint:` line matching the one above. + +```powershell +# 2. Check the installer against the list +$file = Get-ChildItem sp410-opensource-windows-*-setup.exe | Select-Object -First 1 +$expected = (Select-String -Path SHA256SUMS.txt -SimpleMatch $file.Name).Line.Split(' ')[0] +$actual = (Get-FileHash $file -Algorithm SHA256).Hash.ToLower() +if ($actual -eq $expected) { "OK $($file.Name)" } else { "FAILED $($file.Name)" } +``` + +It should print `OK`. On Linux/macOS: +`sha256sum --check --ignore-missing SHA256SUMS.txt`. + +### Authenticode (once releases are code-signed) + +When releases carry an Authenticode signature, you can also check it: +right-click the installer → **Properties → Digital Signatures**, or + +```powershell +Get-AuthenticodeSignature .\sp410-opensource-windows-*-setup.exe | Format-List Status, SignerCertificate +``` + +`Status` should be `Valid`. This complements, but does not replace, the +OpenPGP check above. + +## If verification fails + +- **`BAD signature`** — the checksum file was altered or corrupted. Do not + install. Re-download it from the GitHub release page; if it still fails, + please open an issue. +- **`Can't check signature: No public key`** — you haven't imported + `KEYS.asc` yet (see above). +- **`WARNING: This key is not certified with a trusted signature!`** — this + is normal. It means you haven't personally certified the key. What matters + is that the line above it says `Good signature` and the fingerprint matches + the one in this document. +- **A checksum `FAILED`** — the download is incomplete or tampered with. + Delete it and download again. + +## Reporting a problem + +If a release is signed by a key with a different fingerprint, or you have +any reason to think the key has been compromised, please report it privately +via GitHub's **Security → Report a vulnerability** on this repository rather +than in a public issue. + +## For maintainers: signing a release + +1. Run the release workflow as usual and wait for it to publish. +2. Download `SHA256SUMS.txt` from the release and check it matches the + workflow log. +3. Sign it with the hardware token (PIN + touch): + ```bash + gpg --local-user C24AC72BAAD8552D5048C8901BF0ABA86493ADA3 --armor --detach-sign SHA256SUMS.txt + gpg --verify SHA256SUMS.txt.asc SHA256SUMS.txt + ``` +4. Upload the signature: + ```bash + gh release upload vX.Y.Z SHA256SUMS.txt.asc + ``` diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 2d6e0c7..1e38713 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -87,6 +87,26 @@ Windows Firewall, and on the other computers add an IPP printer at `http://:8631/ipp/print`. macOS, Linux, iOS and Android can print to it too, because it is a standard IPP Everywhere printer. +> **Security note on sharing.** The IPP endpoint has no authentication, so +> `listen = 0.0.0.0` exposes unauthenticated printing, job cancellation and +> job metadata (user and job names) to everyone who can reach the port, in +> cleartext. The service logs a warning when it starts in this mode. Only +> share on a trusted network, scope the firewall rule to the intended subnet, +> and consider `max-connections` to bound load. + +## Security notes + +- **Settings are admin-writable only.** The installer restricts + `C:\ProgramData\SP410` so only SYSTEM and Administrators can change + `sp410.ini` (other users can read it). The service runs as SYSTEM and + re-reads this file every job, so this prevents a standard user from + redirecting it. To edit settings, run Notepad **as administrator**. +- **The `file:` device is off by default.** Writing jobs to a file path is a + debugging feature; enable it deliberately with `[device] allow-file-device = true`. + The `sp410-cli` tool always allows `file:` since you run it yourself. +- **`max-connections`** (`[server]`, default 64) caps simultaneous + connections; excess clients receive HTTP 503. + ## Troubleshooting | Symptom | Check | diff --git a/installer/sp410.ini b/installer/sp410.ini index 59902d4..6e1d638 100644 --- a/installer/sp410.ini +++ b/installer/sp410.ini @@ -14,6 +14,8 @@ model = SP410 ; debug, info, warn or error log-level = info max-job-mb = 64 +; maximum simultaneous connections before new ones are refused +max-connections = 64 [device] ; usb first iDPRT USB printer (default) @@ -26,6 +28,10 @@ uri = usb stall-timeout = 120 ; seconds to keep retrying when the printer is off or unplugged retry-timeout = 300 +; allow the file: device scheme in the service (writes jobs to a file path). +; Off by default: a file: device lets whoever can edit this file have the +; service write to an arbitrary path. Only enable for debugging. +allow-file-device = false [media] ; default label size: na_index-4x6_4x6in, oe_4x2-label_4x2in, om_100x150-label_100x150mm ... diff --git a/installer/sp410.nsi b/installer/sp410.nsi index 96179d1..8df0c0a 100644 --- a/installer/sp410.nsi +++ b/installer/sp410.nsi @@ -180,6 +180,19 @@ Section "Driver service (required)" SecCore ${EndIf} SetOutPath "$INSTDIR" + ; Harden the config/log directory. The service runs as LocalSystem and + ; re-reads sp410.ini for every job, so a non-admin who could modify it could + ; redirect the SYSTEM service. Remove inheritance and grant: + ; SYSTEM (S-1-5-18) and Administrators (S-1-5-32-544) = Full, + ; Users (S-1-5-32-545) = Read & execute only. + ; Well-known SIDs are used so this is locale-independent. + DetailPrint "Securing $APPDATA\SP410 (config is admin-writable only)..." + nsExec::ExecToLog 'icacls "$APPDATA\SP410" /inheritance:r /grant:r "*S-1-5-18:(OI)(CI)F" "*S-1-5-32-544:(OI)(CI)F" "*S-1-5-32-545:(OI)(CI)RX"' + Pop $0 + ${If} $0 != 0 + DetailPrint "warning: could not set ACLs on $APPDATA\SP410 (icacls returned $0)" + ${EndIf} + DetailPrint "Registering the ${APPNAME} service..." nsExec::ExecToLog 'sc.exe query ${SVCNAME}' Pop $0 @@ -227,6 +240,8 @@ Section "Start menu shortcuts" SecShortcuts SetShellVarContext all CreateDirectory "$SMPROGRAMS\${APPNAME}" WriteINIStr "$SMPROGRAMS\${APPNAME}\Printer status.url" "InternetShortcut" "URL" "http://127.0.0.1:${PORT}/" + ; Note: sp410.ini is admin-writable only (see the ACL step above), so saving + ; edits requires running Notepad elevated ("Run as administrator"). CreateShortcut "$SMPROGRAMS\${APPNAME}\Edit printer settings.lnk" "$WINDIR\notepad.exe" '"$APPDATA\SP410\sp410.ini"' CreateShortcut "$SMPROGRAMS\${APPNAME}\Print a test label.lnk" "$INSTDIR\sp410-cli.exe" "test-label" CreateShortcut "$SMPROGRAMS\${APPNAME}\Calibrate label size.lnk" "$INSTDIR\sp410-cli.exe" "calibrate" diff --git a/src/cli/main.c b/src/cli/main.c index 4068174..b3b8d48 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -81,7 +81,7 @@ open_dev(const opts_t *o) if (o->dry_run) return NULL; - if ((d = device_open(o->device, 0, err, sizeof(err))) == NULL) + if ((d = device_open(o->device, 0, 1, err, sizeof(err))) == NULL) /* CLI is interactive: file: allowed */ fprintf(stderr, "sp410-cli: %s\n", err); return d; } diff --git a/src/core/pwg.c b/src/core/pwg.c index 708dc32..02f64d8 100644 --- a/src/core/pwg.c +++ b/src/core/pwg.c @@ -42,10 +42,15 @@ #define OFF_TOTAL_PAGE_COUNT 452 #define OFF_PAGE_SIZE_NAME 1732 -/* Sanity limits: generous for a 108 mm x 300 mm label at up to 600 dpi. */ -#define MAX_WIDTH 20000u -#define MAX_HEIGHT 100000u -#define MAX_BYTES_PER_LINE (20000u * 8u) +/* + * Sanity limits. A 108 mm x 300 mm label at 600 dpi is ~2551 x 7087 px, so + * these leave generous headroom while bounding how much memory one page can + * force the service to allocate (render builds an 8-bit plane of width*height). + */ +#define MAX_WIDTH 10000u +#define MAX_HEIGHT 40000u +#define MAX_BYTES_PER_LINE (10000u * 8u) +#define MAX_PIXELS 64000000u /* 64 Mpx per page */ static unsigned get_u32(const pwg_reader_t *r, const uint8_t *p) @@ -150,6 +155,7 @@ pwg_next_page(pwg_reader_t *r, pwg_header_t *h) r->pos += PWG_HEADER_SIZE; if (!h->width || !h->height || h->width > MAX_WIDTH || h->height > MAX_HEIGHT || + (uint64_t)h->width * h->height > MAX_PIXELS || !h->bits_per_pixel || h->bits_per_pixel > 64 || h->bytes_per_line != (h->width * h->bits_per_pixel + 7) / 8 || h->bytes_per_line > MAX_BYTES_PER_LINE) diff --git a/src/platform/device.c b/src/platform/device.c index f8f6e04..cfb5308 100644 --- a/src/platform/device.c +++ b/src/platform/device.c @@ -458,11 +458,18 @@ handle_read(device_t *d, void *buf, size_t len, int timeout_ms) /* ======================================================================== */ device_t * -device_open(const char *uri, int job_id, char *err, size_t errlen) +device_open(const char *uri, int job_id, int allow_file, char *err, size_t errlen) { device_t *d = calloc(1, sizeof(*d)); int rc; + if (uri && !strncmp(uri, "file:", 5) && !allow_file) + { + seterr(err, errlen, "the file: device is disabled; set [device] " + "allow-file-device = true to enable it"); + free(d); + return NULL; + } if (!d) return NULL; d->sock = SOCK_INVALID; diff --git a/src/platform/device.h b/src/platform/device.h index 2a87bce..65f0617 100644 --- a/src/platform/device.h +++ b/src/platform/device.h @@ -25,7 +25,7 @@ typedef struct device_s device_t; /* Returns non-zero to abort a blocking write (job cancelled / shutting down). */ typedef int (*device_cancel_fn)(void *ctx); -device_t *device_open(const char *uri, int job_id, char *err, size_t errlen); +device_t *device_open(const char *uri, int job_id, int allow_file, char *err, size_t errlen); /* * Write all bytes. A write that makes no progress for `stall_ms` (printer out diff --git a/src/server/config.c b/src/server/config.c index 82a0204..18a7dcf 100644 --- a/src/server/config.c +++ b/src/server/config.c @@ -25,6 +25,7 @@ config_defaults(config_t *c) strcpy(c->model, "SP410"); strcpy(c->log_level, "info"); c->max_job_mb = 64; + c->max_conns = 64; strcpy(c->device, "usb"); c->stall_timeout_s = 120; c->retry_timeout_s = 300; @@ -65,6 +66,17 @@ copy(char *dst, size_t size, const char *src) snprintf(dst, size, "%s", src); } +static int +cfg_bool(const char *key, const char *v, int def) +{ + if (!strcasecmp(v, "true") || !strcasecmp(v, "yes") || !strcasecmp(v, "on") || !strcmp(v, "1")) + return 1; + if (!strcasecmp(v, "false") || !strcasecmp(v, "no") || !strcasecmp(v, "off") || !strcmp(v, "0")) + return 0; + log_msg(LOG_WARN, "config: ignoring %s=\"%s\" (expected true/false)", key, v); + return def; +} + static int to_int(const char *key, const char *v, int lo, int hi, int def) { @@ -140,6 +152,7 @@ config_load(config_t *c, const char *path) else if (!strcasecmp(key, "log-level")) copy(c->log_level, sizeof(c->log_level), val); else if (!strcasecmp(key, "log-file")) copy(c->log_file, sizeof(c->log_file), val); else if (!strcasecmp(key, "max-job-mb")) c->max_job_mb = to_int(key, val, 1, 2048, c->max_job_mb); + else if (!strcasecmp(key, "max-connections")) c->max_conns = to_int(key, val, 1, 4096, c->max_conns); else log_msg(LOG_WARN, "config %s:%d: unknown [server] key \"%s\"", path, lineno, key); } else if (!strcasecmp(section, "device")) @@ -147,6 +160,7 @@ config_load(config_t *c, const char *path) if (!strcasecmp(key, "uri")) copy(c->device, sizeof(c->device), val); else if (!strcasecmp(key, "stall-timeout")) c->stall_timeout_s = to_int(key, val, 1, 86400, c->stall_timeout_s); else if (!strcasecmp(key, "retry-timeout")) c->retry_timeout_s = to_int(key, val, 0, 86400, c->retry_timeout_s); + else if (!strcasecmp(key, "allow-file-device")) c->allow_file_device = cfg_bool(key, val, c->allow_file_device); else log_msg(LOG_WARN, "config %s:%d: unknown [device] key \"%s\"", path, lineno, key); } else if (!strcasecmp(section, "media")) @@ -196,6 +210,8 @@ static const char template_text[] = "; debug, info, warn or error\n" "log-level = info\n" "max-job-mb = 64\n" +"; maximum simultaneous connections before new ones are refused\n" +"max-connections = 64\n" "\n" "[device]\n" "; usb first iDPRT USB printer (default)\n" @@ -208,6 +224,10 @@ static const char template_text[] = "stall-timeout = 120\n" "; seconds to keep retrying when the printer is off or unplugged\n" "retry-timeout = 300\n" +"; allow the file: device scheme in the service (writes jobs to a file path).\n" +"; Off by default: a file: device lets whoever can edit this file have the\n" +"; service write to an arbitrary path. Only enable for debugging.\n" +"allow-file-device = false\n" "\n" "[media]\n" "; default label size: na_index-4x6_4x6in, oe_4x2-label_4x2in, om_100x150-label_100x150mm ...\n" diff --git a/src/server/config.h b/src/server/config.h index 291ecc4..7a8a560 100644 --- a/src/server/config.h +++ b/src/server/config.h @@ -24,11 +24,13 @@ typedef struct char log_level[16]; char log_file[512]; int max_job_mb; + int max_conns; /* concurrent connection cap */ /* [device] */ char device[512]; int device_locked; /* set by --device: ignore [device] uri on reload */ int stall_timeout_s; /* give up when the printer accepts no data this long */ int retry_timeout_s; /* keep retrying an unreachable printer this long */ + int allow_file_device; /* permit the file: device scheme in the service (default off) */ /* [media] */ char default_media[96]; /* [print]: passed to settings_apply() in file order */ diff --git a/src/server/http.c b/src/server/http.c index 7a5c6a8..41f56ae 100644 --- a/src/server/http.c +++ b/src/server/http.c @@ -218,7 +218,19 @@ http_read_request(http_conn_t *c, http_req_t *req, size_t max_body, int idle_ms) return HTTP_BAD; if (sz == 0) break; - if ((rc = grow(req, &cap, req->body_len + sz, max_body)) != 0) + /* + * Bound the chunk against the remaining allowance BEFORE any addition, + * so req->body_len + sz can never wrap SIZE_MAX (which would let a tiny + * allocation be followed by a huge read_exact - a heap overflow). + * The invariant body_len <= max_body is maintained by grow(), so + * max_body - body_len does not underflow. + */ + if (sz > (unsigned long long)(max_body - req->body_len)) + { + req->keep_alive = 0; + return HTTP_TOO_LARGE; + } + if ((rc = grow(req, &cap, req->body_len + (size_t)sz, max_body)) != 0) { req->keep_alive = 0; return rc; @@ -271,6 +283,8 @@ http_respond(http_conn_t *c, int status, const char *reason, const char *ctype, "Content-Type: %s\r\n" "Content-Length: %zu\r\n" "Cache-Control: no-store\r\n" + "X-Content-Type-Options: nosniff\r\n" + "Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'\r\n" "Connection: %s\r\n\r\n", status, reason, ctype ? ctype : "text/plain", len, keep_alive ? "keep-alive" : "close"); diff --git a/src/server/server.c b/src/server/server.c index 1303d37..952c14b 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -33,6 +33,7 @@ #endif #define MAX_JOBS 100 +#define MAX_CONNS 64 /* concurrent connections; excess is refused */ #define IDLE_TIMEOUT_MS 60000 #define CREATE_JOB_TTL_MS 300000 @@ -898,10 +899,12 @@ status_page(server_t *s, const char *host, size_t *len) if (!p) return NULL; #define EMIT(...) do { if (n < cap) n += (size_t)snprintf(p + n, cap - n, __VA_ARGS__); } while (0) + char hostesc[600]; mutex_lock(&s->lock); html_escape(name, sizeof(name), s->cfg.name); html_escape(dev, sizeof(dev), s->cfg.device); html_escape(msg, sizeof(msg), s->message); + html_escape(hostesc, sizeof(hostesc), host); /* client Host header - must be escaped */ EMIT("" "%s