Stirling PDF puts more than 50 PDF operations behind one interface: edit, merge, split, rotate, compress, OCR, sign, redact, watermark, add or remove passwords, and convert to and from Office formats, images and HTML. It runs as a desktop app, in a browser against your own server, or as a REST API, and it can chain steps into no-code pipelines for batch work. The point is that documents never go to an online PDF site.
It is one of the most-starred PDF projects on GitHub, at about 93,000 stars, and is now run by Stirling PDF Inc. Its licence needs a careful read: GitHub shows it as unrecognised because the repository is open core. Code outside a set of named folders is MIT, while folders including app/proprietary, the desktop frontend and engine/ are under a proprietary user licence. In practice the free plan covers all PDF operations for up to five users, and larger teams pay.
- Repository: github.com/Stirling-Tools/Stirling-PDF
- Licence: custom (Other)
- Language: Java. Stars: 93.5K. Forks: 10.2K. Last push: Oct 2, 2026.
- Scan: safe, Oct 2, 2026, commit 302367d
Who it is for
Anyone who handles contracts, scans or forms and would rather not upload them to an online PDF tool, small offices that want one shared PDF server, and developers who need a self-hosted PDF API.
Getting started
1. Run the server with Docker
docker run -p 8080:8080 docker.stirlingpdf.com/stirlingtools/stirling-pdf2. Open the web interface
open http://localhost:80803. Or build from source with the Task runner
git clone https://github.com/Stirling-Tools/Stirling-PDF.git && cd Stirling-PDF && task devDesktop installers and Kubernetes instructions are in the documentation at docs.stirlingpdf.com. Production or business use of the proprietary parts beyond the free plan's limits requires a paid licence.
Safety scan
We cloned Stirling-Tools/Stirling-PDF at commit 302367d on Oct 2, 2026 and ran the checks described on the GitHub Tools page: credential patterns, decode-and-execute code, install-time scripts, committed binaries, risky CI workflows, every host the code talks to, known vulnerabilities in pinned dependencies, and project hygiene. A person read every hit. This is what we found.
- One secret candidate in 7,963 files and about 1.8 million lines: a throwaway private key in a Java database test under app/proprietary. Of the 21 bare-IP URLs, 20 are tests; the other is the desktop app's connection diagnostic, which sends a HEAD request to Cloudflare's 1.1.1.1 to check whether you are online.
- Three pattern hits, all routine: an embedded Dockerfile that installs Node.js from NodeSource with curl | bash during an image build, a startup script that checks whether a port is open with bash's /dev/tcp, and a test fixture. The nine installer scripts are test-environment launchers, none using sudo. One committed binary, the standard Gradle wrapper jar.
- 60 known advisories across 2,081 packages, none critical. The Python engine's uv.lock has pypdf memory and run-time limits on malformed PDFs and urllib3; the Tauri desktop build's Cargo.lock has quick-xml and unmaintained-crate notices; the frontend's package-lock has 23, mostly build tooling. The Java server, the core of the Docker image, uses Gradle without a lockfile, so its libraries were not checked.
- LICENSE is MIT for most of the tree but assigns app/proprietary, engine/, the desktop frontend and other named folders to the Stirling PDF User License, which limits production use beyond trial and minimal use. GitHub reports the licence as unrecognised for that reason.
- 43 workflows. Four use pull_request_target and none checks out the pull request; all 171 third-party actions are pinned to commits. Security policy, Dependabot, CodeQL, licence and contributing guide present, and an OpenSSF Scorecard of 7 out of 10.
What the scanner counted
| Check | Result |
|---|---|
| Secrets | 1 candidate found and read; see the notes above. |
| Suspicious code | 3 pattern hits found and read; every one is listed under the raw findings. |
| Install-time code | 1 Cargo build script. 9 installer scripts |
| Committed binaries | 1 executable or compiled object committed; listed under the raw findings. |
| CI workflows | 43 workflows. 4 use pull_request_target, none check out the pull request head. 0 of 171 third-party actions pinned to a tag rather than a commit. |
| Network hosts | 40 distinct hosts referenced from source; most often stirling.com, pdf.example.com, github.com, docs.stirlingpdf.com. 21 URLs to a bare IP address, listed under the raw findings. |
| Known vulnerabilities | 60 advisories across 2,081 pinned packages: 0 critical, 28 high, 16 moderate, 4 low, 12 unrated. devTools/package-lock.json: 120 packages, 1 advisories; engine/uv.lock: 116 packages, 16 advisories; frontend/editor/src-tauri/Cargo.lock: 667 packages, 21 advisories; frontend/editor/src-tauri/provisioner/Cargo.lock: 12 packages, 0 advisories; frontend/editor/src-tauri/thumbnail-handler/Cargo.lock: 20 packages, 0 advisories; frontend/package-lock.json: 1,302 packages, 23 advisories. |
| Project hygiene | Has security policy, automated dependency updates, CodeQL, licence file, contributing guide. |
| OpenSSF Scorecard | 7.0 out of 10, as of Oct 2, 2026. |
The raw findings
Every hit the scanner wrote out, with a link to the exact line at the scanned commit. Secrets candidates are redacted.
Secret candidates (1, redacted)
| Where | Rule | Match |
|---|---|---|
| app/proprietary/src/test/java/stirling/software/proprietary/policy/asset/JpaPolicyAssetStoreDbTest.java:63 | private-key | -----B…--- (27 chars) |
Pattern hits (3)
| Where | Rule | Match |
|---|---|---|
| app/common/src/test/java/stirling/software/common/util/OfficeDocumentSanitizerTest.java:30 | exfil-host (test/example) | private static final String EXTERNAL_URL = "https://webhook.site/ssrf-callback"; |
| docker/embedded/Dockerfile:15 | download-piped-to-shell | && curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR_VERSION}.x" | bash - \ |
| scripts/init-without-ocr.sh:182 | raw-socket-shell | run_with_timeout "$timeout_secs" bash -c "exec 3<>/dev/tcp/${host}/${port}" 2>/dev/null |
URLs to bare IP addresses (21)
Installer scripts (9)
- frontend/scripts/dev-update-test/setup-dev-updater.sh, 48 lines
- scripts/db-migration/run-migration-test.sh, 250 lines
- scripts/installFonts.sh, 71 lines
- testing/compose/run-multinode-regression.sh, 95 lines
- testing/compose/start-mcp-test.sh, 291 lines
- testing/compose/start-multinode-test.sh, 103 lines
- testing/compose/start-oauth-test.sh, 197 lines
- testing/compose/start-saml-test.sh, 270 lines
- testing/cucumber/run-parallel.sh, 146 lines
Committed binaries (1)
gradle/wrapper/gradle-wrapper.jar: JAR, 48 KB
Worst known vulnerabilities (24 of 60)
| Advisory | Severity | Package | Summary |
|---|---|---|---|
| GHSA-vfj7-8cjw-p6xm | high | braces@3.0.3 | braces vulnerable to stack-exhaustion denial of service through deeply nested patterns |
| GHSA-8xx6-hgc6-gc2m | high | httpx2@2.10.0 | HTTPX2: Streaming response decompression does not bound peak memory (decompression amplification) |
| GHSA-5jq2-8x83-x246 | high | pypdf@6.16.1 | pypdf: Possible long runtimes/large memory usage when parsing indirect objects |
| GHSA-fp3h-c4fm-7vvf | high | pypdf@6.16.1 | pypdf: Possible large memory usage for large /ToUnicode streams (Follow-up 2) |
| GHSA-g9cg-prrw-2r8q | high | pypdf@6.16.1 | pypdf: Possible large memory usage when parsing font data |
| GHSA-jw7q-gvrg-4vj3 | high | pypdf@6.16.1 | pypdf: Possible long runtimes for partially malformed FlateDecode streams (Follow-up) |
| GHSA-php9-fj8v-98fj | high | pypdf@6.16.1 | pypdf: Possible long runtimes when generating appearance streams |
| GHSA-qv6h-rv94-w285 | high | pypdf@6.16.1 | pypdf: Possible large memory usage when retrieving Roman page labels |
| GHSA-v247-6f48-mgcj | high | pypdf@6.16.1 | pypdf: Possible long runtimes with large amount of embedded files |
| GHSA-w23x-9jrw-r45c | high | pypdf@6.16.1 | pypdf: Possible large memory usage when retrieving alphabetical page labels |
| GHSA-8988-9cw3-xx77 | high | urllib3@2.7.0 | urllib3: HTTPS proxy TLS configuration may be ignored or overridden |
| GHSA-vxq7-64xx-v4gw | high | urllib3@2.7.0 | urllib3: HTTPResponse.stream()/read_chunked() buffers an unbounded chunk-size line into memory |
| RUSTSEC-2026-0194 | high | quick-xml@0.37.5 | Quadratic run time when checking a start tag for duplicate attribute names |
| RUSTSEC-2026-0195 | high | quick-xml@0.37.5 | Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service |
| RUSTSEC-2026-0194 | high | quick-xml@0.38.4 | Quadratic run time when checking a start tag for duplicate attribute names |
| RUSTSEC-2026-0195 | high | quick-xml@0.38.4 | Unbounded namespace-declaration allocation in `NsReader` enables memory-exhaustion denial of service |
| GHSA-3pq3-5fj3-cg6v | high | axios@1.18.1 | Axios: HTTP/2 adapter bypasses configured DNS lookup and proxy controls |
| GHSA-542g-h47m-68v8 | high | axios@1.18.1 | Axios: Denial of Service via Unhandled 'error' Event in HTTP/2 ClientHttp2Session Initialization |
| GHSA-c29m-xwm3-cm6r | high | axios@1.18.1 | Axios: ReDoS in fromDataURI data: URL parser freezes the Node event loop (DoS) |
| GHSA-m8m8-qj5v-23w3 | high | axios@1.18.1 | Axios: Node HTTP adapter prototype-pollution gadget allows request socket hijack via inherited createConnection |
| GHSA-mghh-pgcx-3jjj | high | axios@1.18.1 | Axios: ReDoS (O(N²)) in shouldBypassProxy host normalization, reachable via untrusted redirect Location |
| GHSA-r4gj-5m52-g5wh | high | axios@1.18.1 | Axios: maxRedirects: 0 is not enforced by the fetch adapter, allowing redirect-based SSRF |
| GHSA-x97p-jq2g-jp4f | high | axios@1.18.1 | Axios: Prototype Pollution Gadget in axios toFormData Options |
| GHSA-c475-qrg2-pj4r | high | basic-ftp@5.3.1 | basic-ftp: Quadratic-time CPU denial of service in Client.list() Unix directory-listing parser (RE_LINE backtracking) |
Workflows worth a look
- .github/workflows/PR-Demo-cleanup.yml: pull_request_target
- .github/workflows/auto-labelerV2.yml: pull_request_target
- .github/workflows/check_toml.yml: pull_request_target
- .github/workflows/pr-conflict-labeler.yml: pull_request_target
By the numbers
| Stars | 93.5K |
|---|---|
| Forks | 10.2K |
| Contributors | 314 |
| Commits | 6,276 |
| Open issues | 409 |
| Open pull requests | 172 |
| Releases | 189 |
| Latest release | v3.0.2 |
| Licence | custom |
| Main language | Java |
| Project age | 3 years |
| Last push | Oct 2, 2026 |
| Tracked files | 7,963 |
| Lines of code | 1.8M |
| Checkout size | 215 MB |
Lines by language: TOML 570K, TypeScript 569.5K, Java 450.8K, JSON 54.2K, CSS 42.3K, Python 39.6K.
Questions
Is Stirling PDF free?
For personal use and small teams, yes. The free plan is self-hosted, includes every PDF operation and OAuth2 single sign-on, and allows up to five users. The Team plan is $99 a month or $999 a year for 100 users, and Enterprise (custom pricing) adds SAML, audit logs, air-gapped licensing and SLAs.
Is Stirling PDF open source?
Partly. It is open core: most of the repository is MIT, but named directories, including the engine, desktop frontend and proprietary app modules, carry a Stirling PDF User License that restricts production use to licence holders beyond trial and minimal use. Check the LICENSE file before building a product on it.
Does Stirling PDF send my files anywhere?
No. When you self-host it or use the desktop app, processing happens on your machine or server, and the project's pitch is that documents never go to an external service. Optional integrations such as Google Drive are paid-plan features you would have to enable.
This post is part of GitHub Tools, where every repository is cloned and scanned before it is written up. The scan is a snapshot of one commit on one day; the repository has moved on since, so check it before you install.
