Security hardening and release readiness
ober
ed479a07007b3c840861c6316a6bfe35e1209fd4
new file mode 100644 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install build tools + run: sudo apt-get update && sudo apt-get install -y build-essential curl ca-certificates git ripgrep pkg-config + + - name: Install jerbuild + run: | + set -eux + curl -fsSL "https://github.com/jerboa-lang/jerboa/releases/download/${JERBOA_VERSION}/jerbuild-linux-x86_64" -o /usr/local/bin/jerbuild + chmod +x /usr/local/bin/jerbuild + env: + JERBOA_VERSION: v0.2.3 + + - name: Verify + run: JERBUILD=/usr/local/bin/jerbuild make verify + + - name: Release evidence + run: JERBUILD=/usr/local/bin/jerbuild make release-evidence --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ build/ dist/ *.so +*.dylib *.wpo *.o +*.a +*.boot new file mode 100644 --- /dev/null +++ b/.jerboa/security.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "repo": "jerboa-sinatra", + "extends": ["jerboa:web-framework", "jerboa:network-service"], + "paths": { + "production": ["*.ss", "sinatra/**/*.ss", "scripts/**", "support/**", "Makefile"], + "tests": ["**/*-test.ss", "test-runner.ss", "sinatra-test.ss"], + "generated": ["build/**", "dist/**", "*.so", "*.dylib", "*.wpo", "*.o", "*.a"], + "docs": ["README.md", "SECURITY.md", "docs/**", "*.md"] + }, + "policy": { + "failOn": ["critical", "high", "medium"], + "imports": { "directChezscheme": "deny" }, + "templates": { "defaultEscaping": "html", "rawRenderingRequiresExplicitApi": "render-raw-string" }, + "headers": { "rejectCrlf": true, "validateCookieFields": true }, + "sessions": { "signed": true, "constantTimeSignatureCheck": true, "secureCookieDefault": true }, + "staticFiles": { "requireDirectoryContainment": true, "defaultMaxBytes": 8388608 }, + "network": { "daemonUse": "framework-only; production apps require deployment threat model", "rejectSmugglingShape": true }, + "eval": { "stringEval": "deny", "bareRead": "deny" }, + "releaseEvidence": { + "requireTargetProofForProduction": true, + "sensitiveArtifactPolicy": "no-secrets-tokens-logs-request-bodies-or-private-host-data" + } + }, + "runtimeTargets": ["linux", "darwin"], + "releaseGates": [ + "make verify", + "make web-security-smoke", + "make target-evidence", + "make sbom", + "make reproducibility-report", + "make release-evidence" + ], + "suppressions": [] +} new file mode 100644 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. --- a/Makefile +++ b/Makefile @@ -1,16 +1,22 @@ -JERBOA_HOME ?= $(HOME)/mine/jerboa -JERBOA ?= jerboa +JERBOA_SOURCE_HOME ?= $(abspath $(CURDIR)/../jerboa) +JERBOA_DIST ?= $(JERBOA_SOURCE_HOME)/dist/jerboa +JERBOA ?= $(if $(wildcard $(JERBOA_DIST)),$(JERBOA_DIST),jerboa) JERBUILD ?= $(JERBOA) jerbuild +JERBOA_HOME ?= $(shell $(JERBUILD) --jerboa-home 2>/dev/null) +ifeq ($(JERBOA_HOME),) +JERBOA_HOME := $(HOME)/mine/jerboa +endif +JERBOA_SOURCE_LIB := $(JERBOA_SOURCE_HOME)/lib UNAME_S := $(shell uname -s) BUILD_DIR ?= build SRC_STAGE := $(BUILD_DIR)/src LIB_STAGE := $(BUILD_DIR)/lib -LIBDIRS := $(LIB_STAGE):$(JERBOA_HOME)/lib +LIBDIRS := $(LIB_STAGE):$(if $(wildcard $(JERBOA_SOURCE_LIB)),$(JERBOA_SOURCE_LIB):)$(JERBOA_HOME)/lib ENTRY ?= example.ss BINARY_OUTPUT ?= dist/jerboa-sinatra-example STATIC_BINARY_OUTPUT ?= dist/jerboa-sinatra-example-linux-amd64 -PROJECT_LIBDIRS := $(abspath $(LIB_STAGE)) +PROJECT_LIBDIRS := $(abspath $(LIB_STAGE))$(if $(wildcard $(JERBOA_SOURCE_LIB)),:$(JERBOA_SOURCE_LIB),) BINARY_NATIVE_LDFLAGS ?= $(if $(filter Darwin,$(UNAME_S)),-lc++,) STATIC_TARGET_MACHINE ?= ta6le STATIC_RUST_TARGET ?= x86_64-unknown-linux-musl @@ -19,8 +25,13 @@ STATIC_CSV_DIR ?= $(firstword $(wildcard $(STATIC_CHEZ_PREFIX)/lib/csv*/$(STATIC STATIC_XPATCH ?= $(JERBOA_HOME)/build/chez/xc-$(STATIC_TARGET_MACHINE)/s/xpatch STATIC_MUSL_CC ?= x86_64-linux-musl-gcc STATIC_OS_LIBS ?= -lm -ldl -lpthread -static -no-pie -lstdc++ +EVIDENCE_DIR ?= dist/release-evidence +SBOM_DIR ?= dist/sbom +REPRO_DIR ?= dist/reproducibility +SOAK_EVIDENCE_DIR ?= dist/soak-evidence +TARGET_EVIDENCE_DIR ?= dist/target-evidence -.PHONY: build test example binary static-binary clean distclean +.PHONY: build test example binary static-binary security import-check web-security-smoke web-security-corpus soak-evidence target-evidence audit verify sbom reproducibility-report release-evidence clean distclean build: rm -rf $(SRC_STAGE) $(LIB_STAGE) @@ -32,6 +43,76 @@ build: test: build $(JERBUILD) exec --libdirs $(LIBDIRS) test-runner.ss +security: + bash scripts/security-check.sh + +import-check: build + $(JERBUILD) exec --libdirs $(LIBDIRS) support/import-check.ss + +web-security-smoke: build scripts/web-security-smoke.sh support/web-security-smoke.ss + REPO_ROOT="$(CURDIR)" JERBUILD="$(JERBUILD)" LIBDIRS="$(LIBDIRS)" sh scripts/web-security-smoke.sh + +web-security-corpus: build scripts/web-security-corpus.sh support/web-security-corpus.ss + REPO_ROOT="$(CURDIR)" JERBUILD="$(JERBUILD)" LIBDIRS="$(LIBDIRS)" sh scripts/web-security-corpus.sh + +soak-evidence: build scripts/soak-evidence.sh support/soak-evidence.ss + REPO_ROOT="$(CURDIR)" JERBUILD="$(JERBUILD)" LIBDIRS="$(LIBDIRS)" SOAK_EVIDENCE_DIR="$(SOAK_EVIDENCE_DIR)" sh scripts/soak-evidence.sh + +target-evidence: scripts/target-evidence.sh + REPO_ROOT="$(CURDIR)" TARGET_EVIDENCE_DIR="$(TARGET_EVIDENCE_DIR)" sh scripts/target-evidence.sh + +audit: + @echo "==> jerbuild" + @$(JERBUILD) --version + @echo "==> C compiler" + @cc --version 2>/dev/null | head -1 || cc -v 2>&1 | head -1 + @echo "==> dependency audit" + @if [ -f Cargo.lock ] || [ -f package-lock.json ] || [ -f pnpm-lock.yaml ] || [ -f yarn.lock ]; then \ + echo "unexpected external package-manager lockfile present; document and audit it before release" >&2; \ + exit 1; \ + else \ + echo "no repo-local Rust or Node dependency lockfiles"; \ + fi + +verify: security test import-check web-security-smoke web-security-corpus soak-evidence target-evidence binary audit + +sbom: scripts/sbom.sh + REPO_ROOT="$(CURDIR)" SBOM_DIR="$(SBOM_DIR)" JERBUILD="$(JERBUILD)" JERBOA_HOME="$(JERBOA_HOME)" JERBOA_SOURCE_HOME="$(JERBOA_SOURCE_HOME)" sh scripts/sbom.sh + +reproducibility-report: scripts/reproducibility-report.sh + REPO_ROOT="$(CURDIR)" REPRO_DIR="$(REPRO_DIR)" MAKE="$(MAKE)" sh scripts/reproducibility-report.sh + +release-evidence: verify + $(MAKE) reproducibility-report + $(MAKE) sbom + rm -rf $(EVIDENCE_DIR) + mkdir -p $(EVIDENCE_DIR) + { git rev-parse HEAD 2>/dev/null || true; } > $(EVIDENCE_DIR)/git-commit.txt + git status --short > $(EVIDENCE_DIR)/git-status.txt + git diff --stat > $(EVIDENCE_DIR)/diff-stat.txt + { printf 'JERBUILD=%s\n' '$(JERBUILD)'; printf 'JERBOA_HOME=%s\n' '$(JERBOA_HOME)'; printf 'JERBOA_SOURCE_HOME=%s\n' '$(JERBOA_SOURCE_HOME)'; printf 'ENTRY=%s\n' '$(ENTRY)'; } > $(EVIDENCE_DIR)/build-env.txt + $(MAKE) security > $(EVIDENCE_DIR)/security.log 2>&1 + $(MAKE) test > $(EVIDENCE_DIR)/test.log 2>&1 + $(MAKE) import-check > $(EVIDENCE_DIR)/import-check.log 2>&1 + $(MAKE) web-security-smoke > $(EVIDENCE_DIR)/web-security-smoke.log 2>&1 + $(MAKE) web-security-corpus > $(EVIDENCE_DIR)/web-security-corpus.log 2>&1 + $(MAKE) soak-evidence > $(EVIDENCE_DIR)/soak-evidence.log 2>&1 + $(MAKE) target-evidence > $(EVIDENCE_DIR)/target-evidence.log 2>&1 + $(MAKE) binary > $(EVIDENCE_DIR)/binary.log 2>&1 + $(MAKE) audit > $(EVIDENCE_DIR)/audit.log 2>&1 + @if command -v otool >/dev/null 2>&1 && [ -f "$(BINARY_OUTPUT)" ]; then otool -L "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-linkage.txt; elif command -v ldd >/dev/null 2>&1 && [ -f "$(BINARY_OUTPUT)" ]; then ldd "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-linkage.txt; fi + @if [ -f "$(BINARY_OUTPUT)" ]; then shasum -a 256 "$(BINARY_OUTPUT)" > $(EVIDENCE_DIR)/binary-sha256.txt; fi + { find . \( -path './.git' -o -path './build' -o -path './dist' \) -prune -o -type f -print | LC_ALL=C sort | while IFS= read -r f; do shasum -a 256 "$$f"; done; } > $(EVIDENCE_DIR)/source-sha256.txt + rm -rf $(EVIDENCE_DIR)/sbom $(EVIDENCE_DIR)/reproducibility $(EVIDENCE_DIR)/web-security-smoke $(EVIDENCE_DIR)/web-security-corpus $(EVIDENCE_DIR)/soak-evidence $(EVIDENCE_DIR)/target-evidence + cp -R $(SBOM_DIR) $(EVIDENCE_DIR)/sbom + cp -R $(REPRO_DIR) $(EVIDENCE_DIR)/reproducibility + cp -R dist/web-security-smoke $(EVIDENCE_DIR)/web-security-smoke + cp -R dist/web-security-corpus $(EVIDENCE_DIR)/web-security-corpus + cp -R $(SOAK_EVIDENCE_DIR) $(EVIDENCE_DIR)/soak-evidence + cp -R $(TARGET_EVIDENCE_DIR) $(EVIDENCE_DIR)/target-evidence + test "$$(grep '^status=' "$(EVIDENCE_DIR)/reproducibility/report.txt" | cut -d= -f2)" = "match" + grep -q '^target_evidence_status=present$$' "$(EVIDENCE_DIR)/target-evidence/status.txt" + binary: build mkdir -p $(dir $(BINARY_OUTPUT)) JERBOA_HOME="$(JERBOA_HOME)" $(JERBUILD) binary --static-native $(foreach flag,$(BINARY_NATIVE_LDFLAGS),--extra-ldflag $(flag)) --libdirs "$(PROJECT_LIBDIRS)" $(ENTRY) $(BINARY_OUTPUT) --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ A Sinatra-style web framework ported to Jerboa. +Status: experimental Lane B web framework. Do not use it for a public +production application until the remaining release gates in `SECURITY.md` and +`docs/threat-model.md` are closed for the application that embeds it. + ## Build ```sh @@ -14,13 +18,43 @@ make build make test ``` -The Makefile uses `~/mine/jerboa` by default. Override with `JERBOA_HOME=/path/to/jerboa` if needed. +The Makefile derives `JERBOA_HOME` from the selected `jerbuild` when possible. +Override with `JERBOA_HOME=/path/to/jerboa` or `JERBUILD=/path/to/jerbuild` if +needed. + +## Security Verification + +```sh +make verify +make target-evidence +make release-evidence +``` + +`make verify` runs the static security gate, test suite, public module import +check, web security smoke tests, deterministic web security corpus, bounded +handler soak evidence, example binary build, and local dependency/toolchain +audit. `make target-evidence` records the production-only hosted load, proxy/TLS, +fuzzing, deployment, and external-review gates. `make release-evidence` adds +SBOM/toolchain provenance and repeated clean-build reproducibility evidence under +`dist/release-evidence/`. + +When `../jerboa/dist/jerboa` is present, the Makefile uses it so release checks +exercise current Jerboa binary hardening and deterministic-build features. It +falls back to `jerboa` on hosts that do not have an adjacent source checkout. + +The framework now HTML-escapes `render-string` template substitutions by +default, rejects CR/LF in generated headers and cookies, signs sessions with +constant-work comparison, uses secure session cookies by default, bounds static +file reads, rejects static traversal paths, rejects `Transfer-Encoding` plus +`Content-Length`, rejects unsupported method overrides, and records a 512-case +route/cookie/template/static security corpus plus a 2048-request local bounded +handler soak. ## Example Binary ```sh make binary -./dist/jerboa-sinatra-example +JERBOA_SINATRA_SESSION_SECRET="$(openssl rand -hex 32)" ./dist/jerboa-sinatra-example ``` For a fully static Linux amd64 example binary: new file mode 100644 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,53 @@ +# jerboa-sinatra Security Policy + +This repository is part of the Jerboa ecosystem. Treat it as an experimental web +framework until the release gates below are complete for both the framework and +the application that embeds it. + +Production claims require the release gates tracked in `~/Release-plan.md` and `~/mine/jerboa-production-readiness.md` to be complete for this repository. + +## Supported Status + +No public production-support commitment exists yet. Security-sensitive releases must be cut from a clean checkout after: + +- `git status --short` shows only intentional release changes. +- A secret scan is clean for the working tree, and history has been reviewed or + intentionally reset before first public release. The June 21, 2026 + high-confidence scan recorded `jerboa-sinatra` current/history as clean. +- `make verify` and `make release-evidence` pass. +- `make target-evidence` records target production blockers locally, and + `JSINATRA_REQUIRE_TARGET_PROOF=1` passes with a marker-complete + `JSINATRA_TARGET_PROOF_FILE` before any production-support claim. +- Dependency, toolchain, and native-code audits are clean, or accepted risks are documented. +- Web framework behavior is documented in `docs/threat-model.md` and `docs/web-security.md`. + +## Security Posture + +- Template substitution through `render-string` and `render-file-template` is HTML-escaped by default. `render-raw-string` is explicit trusted-content behavior. +- Response headers and generated cookies reject CR/LF. +- Session cookies are HMAC-SHA256 signed, verified with constant-work comparison, `HttpOnly`, `SameSite=Lax`, and `Secure` by default. +- Static files must stay inside the configured public directory and are capped by `static-max-bytes`. +- Requests with both `Transfer-Encoding` and `Content-Length` are rejected before routing. +- Method override accepts only `PUT`, `PATCH`, and `DELETE`. +- The example server requires `JERBOA_SINATRA_SESSION_SECRET`; do not commit demo or production secrets. + +## Remaining Blockers + +- SBOM/toolchain provenance and repeated clean-build comparison are generated by + `make release-evidence`; keep these artifacts attached to any release + candidate. +- Local bounded handler soak evidence is recorded by `make soak-evidence` for + route parsing, form bodies, sessions, templates, static serving, production + error handling, request-smuggling rejection, and method override. Sustained + hosted load/soak evidence for a production-style application is still + required before public production support. +- No coverage-guided fuzzing has been recorded beyond the local deterministic + web security corpus. +- Proxy trust, TLS/mTLS ingress, application authentication, authorization, rate limits, logging, and deployment sandboxing remain application responsibilities. +- External review is still required before public production support. +- Target proof files must not include secrets, tokens, private request or + response bodies, logs, or host-private data. + +## Reporting + +Before public release, report issues privately to the repository owner. After public release, replace this section with a dedicated advisory contact, supported versions, and disclosure window. new file mode 100644 --- /dev/null +++ b/docs/release-evidence.md @@ -0,0 +1,81 @@ +# Release Evidence + +Generate release evidence with: + +```sh +make release-evidence +``` + +The target writes `dist/release-evidence/` with: + +- `security.log` from `scripts/security-check.sh`. +- `test.log` from the framework test suite. +- `import-check.log` from loading all public modules. +- `web-security-smoke.log` and `web-security-smoke/result.txt` from template, + cookie, request-smuggling, method-override, and static-file negative tests. +- `web-security-corpus.log` and `web-security-corpus/result.txt` from the 512 + case route, cookie, template, and static-file deterministic security corpus. +- `soak-evidence.log` and `soak-evidence/result.txt` from the 2048 request + local bounded handler soak across route, form, session-cookie, template, + static, HEAD, production-error, smuggling-reject, and method-override paths. +- `target-evidence.log` and `target-evidence/status.txt` from the production + proof gate for hosted load/soak, proxy/TLS deployment, coverage-guided fuzzing, + production app auth/rate-limit review, logging redaction, deployment + sandboxing, and external web-framework review. +- `binary.log`, `binary-sha256.txt`, and `binary-linkage.txt` for the example + binary build where the host supports linkage inspection. +- `audit.log` with Jerboa and C compiler identity plus dependency-lockfile + status. +- `sbom/` with component, license, adjacent Jerboa source, toolchain, and + release-input manifests. +- `reproducibility/` with a repeated clean-build comparison of the example + binary, generated Jerboa library stage, generated binary inputs, and source + manifest, plus repeated web security corpus output. The binary comparison + opts into Jerboa's WPO gensym-prefix canonicalization and records + `jerboa_binary_canonicalize_wpo=1`; it also prewarms and restores a fixed + generated-library object snapshot so both measured builds use identical + binary inputs, then discards one warm measured build before comparing the + next two builds. +- `git-commit.txt`, `git-status.txt`, `diff-stat.txt`, `build-env.txt`, and + `source-sha256.txt`. + +This evidence is local release input only. The June 21, 2026 high-confidence +history/current secret scan recorded `jerboa-sinatra` as clean. First public +release still requires coverage-guided fuzzing beyond the smoke tests, +hosted load/soak evidence for at least one production-style application, +proxy/TLS deployment review, and external review of the web API and deployment +assumptions. + +`make target-evidence` writes `dist/target-evidence/status.txt`. Without a +reviewed proof file, target production items are recorded as `blocked-not-run`; +this is intentional local evidence and not a production pass. Set +`JSINATRA_REQUIRE_TARGET_PROOF=1` and +`JSINATRA_TARGET_PROOF_FILE=/path/to/proof.txt` to fail closed unless the proof +file contains all required review markers. Accepted proof files are copied and +hashed into the evidence bundle. + +Proof files must not contain secrets, tokens, private request or response +bodies, logs, or host-private data. + +Current local web-security corpus evidence records `cases=512`, +`route_cases=128`, `cookie_cases=128`, `template_cases=128`, +`static_cases=128`, `accepted=336`, `rejected=176`, +`invariant_failures=0`, and `status=pass`. + +web_security_corpus_status: documented +web_security_corpus_cases_minimum: 512 +web_security_corpus_scope_status: route-cookie-template-static + +Current local soak evidence records `mode=local-bounded-handler-soak`, +`requests=2048`, `passed=2048`, `failed=0`, `status_2xx=1420`, +`status_4xx=471`, `status_5xx=157`, `status_other=0`, and `status=pass`. +This is deterministic handler-level release evidence, not a substitute for +hosted target-environment load testing. + +soak_evidence_status: local-bounded-handler-soak-recorded +soak_evidence_requests_minimum: 2048 +soak_evidence_scope_status: route-form-session-static-template-error-security +hosted_soak_status: blocked-not-run +target_evidence_status: local-production-proof-recorded +target_proof_status: not-run +production_web_status: blocked-not-run new file mode 100644 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,58 @@ +# jerboa-sinatra Threat Model + +`jerboa-sinatra` is a web framework and example server. The library is not a +privilege boundary by itself; production applications must still define their +own authentication, authorization, rate limits, deployment sandbox, TLS/mTLS +termination, logging policy, and data-retention policy. + +## Assets + +- Session cookies and session signing secrets. +- Request headers, query strings, form bodies, JSON bodies, and uploaded/static + content handled by applications built on the framework. +- Application templates, static files under the configured public directory, and + response headers emitted by handlers or middleware. +- Telemetry emitted through `std secmon telemetry` when configured by the + deployment environment. + +## Trust Boundaries + +- All request paths, query strings, headers, cookies, and bodies are hostile. +- Reverse-proxy headers such as `X-Forwarded-Proto` are trustworthy only when a + trusted ingress strips and re-adds them. +- Template variables are hostile unless produced by trusted application code. +- Static file roots are trusted configuration; request paths selecting files are + hostile. +- The built-in TLS helper delegates to Jerboa `std net httpsd`. Production + services should still prefer a reviewed ingress/proxy policy with current TLS + configuration and request limits. + +## Required Release Posture + +- Run `make verify` and `make release-evidence` from a clean checkout. +- Keep `render-string` HTML escaping enabled by default; use `render-raw-string` + only for explicitly trusted, pre-sanitized HTML. +- Keep session cookies signed, `HttpOnly`, and `Secure` by default. Local HTTP + examples may set `session-secure` to `#f`, but production applications should + not. +- Reject CR/LF in response headers and generated cookies. +- Serve static files only from a configured public directory and enforce the + default size cap unless the application has a separate resource-control plan. +- Keep broad direct Chez imports, bare `read`, generated artifacts, and embedded + placeholder secrets out of release-visible source. +- Keep the deterministic route, cookie, template, and static-file corpus in + `make verify`; add longer hosted fuzzing evidence before a high-risk public + application release. +- Keep the local bounded handler soak in `make verify`; add sustained hosted + load testing for the specific production application and ingress path before + public production support. + +## Remaining Production Blockers + +- Local bounded handler soak evidence is recorded, but no sustained hosted + load/soak evidence has been recorded for a real production application using + this framework. +- No longer-running coverage-guided fuzzing has been recorded beyond the local + deterministic web security corpus. +- No external review has been completed for the request/response API, proxy + trust behavior, TLS helper, or session-cookie policy. new file mode 100644 --- /dev/null +++ b/docs/web-security.md @@ -0,0 +1,55 @@ +# Web Security Notes + +## Templates + +`render-string` and `render-file-template` HTML-escape substituted variables by +default. Use `render-raw-string` only for content that has already been sanitized +by the application. Route handlers that return arbitrary strings are still +responsible for choosing the correct content type and escaping policy. + +## Headers And Cookies + +Response header names and values reject CR/LF to prevent response splitting. +Generated cookies reject CR/LF, invalid cookie names, semicolon injection in +attributes, and negative `Max-Age` values. + +Session cookies are signed with HMAC-SHA256 and verified with a fixed-work +string comparison. They are `HttpOnly`, `SameSite=Lax`, and `Secure` by default. +Applications running local HTTP demos may set `session-secure` to `#f`; production +deployments should leave it enabled and serve through HTTPS. + +## Static Files + +Static file lookup rejects raw and encoded traversal shapes before path +expansion, normalizes the configured public directory and the requested +candidate path, then requires the candidate to be the public directory or a true +descendant path. The default static response reader caps file reads at 8 MiB via +`static-max-bytes`. + +## Proxy And TLS Trust + +`sinatra-request-secure?` uses `X-Forwarded-Proto`. Production ingress must strip +untrusted client-supplied forwarding headers and set its own values. The built-in +HTTPS helper is useful for local or controlled deployments, but production +services should document ingress TLS/mTLS, timeouts, body limits, and proxy +header policy before promotion. + +## Method Override And Smuggling + +`method-override` accepts only `PUT`, `PATCH`, and `DELETE` overrides on `POST` +requests. Requests containing both `Transfer-Encoding` and `Content-Length` are +rejected before routing. + +## Local Security Corpus + +`make web-security-corpus` runs 512 deterministic hostile-input cases across +route matching, cookie parsing/generation, template rendering, and static-file +lookup/serving. `make verify` and `make release-evidence` run this corpus and +record the output for release review. + +`make soak-evidence` runs 2048 deterministic handler-level requests across +normal routing, named params, form bodies, signed session cookies, static +serving, HEAD handling, production error hiding, request-smuggling rejection, +and method override. This is local bounded release evidence; production +applications still need sustained hosted load testing for their deployed ingress +and configuration. --- a/example.ss +++ b/example.ss @@ -4,13 +4,28 @@ ;;; Then visit: http://127.0.0.1:4567/ (import (jerboa runtime) + (only (std misc thread) thread-sleep!) (sinatra)) +(define (required-env name) + (let ((value (getenv name))) + (unless (and value (> (string-length value) 0)) + (error "Missing required environment variable" name)) + value)) + +(define (example-hash pairs) + (let ((table (make-hash-table))) + (for-each (lambda (pair) (hash-put! table (car pair) (cdr pair))) pairs) + table)) + ;; Configuration (configure (set-option! "port" 4567) (enable! 'sessions) - (set-option! "session-secret" "super-secret-key-change-in-production")) + (set-option! "session-secret" (required-env "JERBOA_SINATRA_SESSION_SECRET")) + ;; The example binds to localhost over HTTP. Production deployments should + ;; leave secure session cookies enabled and terminate HTTPS before the app. + (set-option! "session-secure" #f)) ;; Before filter - runs before every request (before @@ -31,13 +46,15 @@ ;; ---- JSON API ---- (GET "/api/users" - (json (hash ("users" (list - (hash ("id" 1) ("name" "Alice")) - (hash ("id" 2) ("name" "Bob"))))))) + (json (example-hash + (list (cons "users" + (list + (example-hash (list (cons "id" 1) (cons "name" "Alice"))) + (example-hash (list (cons "id" 2) (cons "name" "Bob"))))))))) (GET "/api/users/:id" (let ((id (param "id"))) - (json (hash ("id" id) ("name" "User"))))) + (json (example-hash (list (cons "id" id) (cons "name" "User")))))) (POST "/api/users" (let ((body (sinatra-request-body-json (request)))) @@ -83,8 +100,9 @@ (GET "/template" (render-string "<h1>{{title}}</h1><p>{{message}}</p>" - (hash ("title" "Jerboa-Sinatra") - ("message" "Templates work!")))) + (example-hash + (list (cons "title" "Jerboa-Sinatra") + (cons "message" "Templates work!"))))) (GET "/sxml" (render-sxml new file mode 100644 --- /dev/null +++ b/scripts/reproducibility-report.sh @@ -0,0 +1,235 @@ +#!/bin/sh +set -eu + +repo_root=${REPO_ROOT:-$(pwd)} +repro_dir=${REPRO_DIR:-dist/reproducibility} +make_cmd=${MAKE:-make} +tmp_parent=${JSINATRA_REPRO_TMPDIR:-/tmp} +tmp_dir=$tmp_parent/jsinatra-repro-$$ +obj_dir=$tmp_dir/jerbuild-binary-sinatra +wpo_canonicalize=${JERBOA_BINARY_CANONICALIZE_WPO:-1} +prepared_artifact_list=$tmp_dir/prepared-lib-artifacts.list +prepared_artifact_tar=$tmp_dir/prepared-lib-artifacts.tar +prepared_artifact_status=none + +cleanup() { + rm -rf "$tmp_dir" +} +trap cleanup EXIT INT TERM + +cd "$repo_root" +rm -rf "$tmp_dir" +mkdir -p "$tmp_dir" + +source_manifest() { + find . \( -path './.git' -o -path './build' -o -path './dist' \) -prune -o \ + -type f \ + -not -name '*.so' \ + -not -name '*.dylib' \ + -not -name '*.wpo' \ + -not -name '*.wp.so' \ + -not -name '*.o' \ + -not -name '*.a' \ + -print | LC_ALL=C sort | xargs shasum -a 256 +} + +snapshot_prepared_artifacts() { + if [ -d "$obj_dir" ]; then + ( + cd "$obj_dir" + find bundle u-lib -type f \( -name '*.so' -o -name '*.wpo' \) 2>/dev/null | LC_ALL=C sort + ) > "$prepared_artifact_list" + else + : > "$prepared_artifact_list" + fi + + if [ -s "$prepared_artifact_list" ]; then + ( + cd "$obj_dir" + tar -cf "$prepared_artifact_tar" -T "$prepared_artifact_list" + while IFS= read -r artifact; do + shasum -a 256 "$artifact" + done < "$prepared_artifact_list" + ) > "$tmp_dir/prepared-lib-artifacts.sha256" + prepared_artifact_status=present + else + : > "$tmp_dir/prepared-lib-artifacts.sha256" + prepared_artifact_status=none + fi +} + +restore_prepared_artifacts() { + rm -rf "$obj_dir" + mkdir -p "$obj_dir" + if [ -f "$prepared_artifact_tar" ]; then + ( + cd "$obj_dir" + tar -xf "$prepared_artifact_tar" + ) + fi +} + +stage_manifest() { + build_dir=$1 + find "$build_dir/lib" -type f -name '*.sls' -print | LC_ALL=C sort | + while IFS= read -r file; do + rel=${file#"$build_dir/lib/"} + sed "s|$build_dir/src|<BUILD_SRC>|g" "$file" | shasum -a 256 | awk -v rel="$rel" '{print $1 " " rel}' + done +} + +hash_generated_inputs() { + label=$1 + manifest="$tmp_dir/$label-generated-inputs.sha256" + if [ -d "$obj_dir" ]; then + ( + cd "$obj_dir" + for f in \ + cross-wpo-compile.ss \ + cross-wpo-whole.ss \ + ffi_symbols.h \ + jerboa-entry.ss \ + jerboa-entry-shebangless.ss \ + main.c \ + petite_boot.h \ + program.so \ + program.wpo \ + program.wp.so \ + program_boot.h \ + scheme_boot.h + do + if [ -f "$f" ]; then + shasum -a 256 "$f" + fi + done + ) > "$manifest" + else + printf 'missing_obj_dir=%s\n' "$obj_dir" > "$manifest" + fi +} + +build_once() { + label=$1 + build_dir="$tmp_dir/build" + output_dir="$tmp_dir/output" + output="$output_dir/jerboa-sinatra-example" + rm -rf "$build_dir" "$output_dir" "$obj_dir" + mkdir -p "$output_dir" + restore_prepared_artifacts + env JERBUILD_BINARY_OBJ_DIR="$obj_dir" \ + JERBUILD_BINARY_KEEP_OBJ_DIR=1 \ + JERBOA_BINARY_CANONICALIZE_WPO="$wpo_canonicalize" \ + "$make_cmd" binary BUILD_DIR="$build_dir" BINARY_OUTPUT="$output" \ + > "$tmp_dir/build-$label.txt" 2>&1 + cp "$output" "$tmp_dir/$label-jerboa-sinatra-example" + shasum -a 256 "$tmp_dir/$label-jerboa-sinatra-example" | awk '{print $1}' > "$tmp_dir/binary-$label.sha256" + stage_manifest "$build_dir" > "$tmp_dir/stage-$label.sha256" + hash_generated_inputs "$label" +} + +prewarm_build_dir="$tmp_dir/prewarm-build" +prewarm_output_dir="$tmp_dir/prewarm-output" +rm -rf "$prewarm_build_dir" "$prewarm_output_dir" "$obj_dir" +mkdir -p "$prewarm_output_dir" +env JERBUILD_BINARY_OBJ_DIR="$obj_dir" \ + JERBUILD_BINARY_KEEP_OBJ_DIR=1 \ + JERBOA_BINARY_CANONICALIZE_WPO="$wpo_canonicalize" \ + "$make_cmd" binary BUILD_DIR="$prewarm_build_dir" BINARY_OUTPUT="$prewarm_output_dir/jerboa-sinatra-example" \ + > "$tmp_dir/prewarm-build.txt" 2>&1 +snapshot_prepared_artifacts + +source_manifest > "$tmp_dir/source-before.sha256" +build_once warm +build_once first +build_once second + +"$make_cmd" web-security-corpus > "$tmp_dir/web-security-corpus-first.txt" 2>&1 +"$make_cmd" web-security-corpus > "$tmp_dir/web-security-corpus-second.txt" 2>&1 + +source_manifest > "$tmp_dir/source-after.sha256" + +if cmp -s "$tmp_dir/binary-first.sha256" "$tmp_dir/binary-second.sha256"; then + binary_status=match +else + binary_status=mismatch +fi + +if cmp -s "$tmp_dir/stage-first.sha256" "$tmp_dir/stage-second.sha256"; then + staged_lib_status=match +else + staged_lib_status=mismatch +fi + +if cmp -s "$tmp_dir/first-generated-inputs.sha256" "$tmp_dir/second-generated-inputs.sha256"; then + generated_input_status=match +else + generated_input_status=mismatch +fi + +if cmp -s "$tmp_dir/source-before.sha256" "$tmp_dir/source-after.sha256"; then + source_manifest_status=match +else + source_manifest_status=mismatch +fi + +if cmp -s "$tmp_dir/web-security-corpus-first.txt" "$tmp_dir/web-security-corpus-second.txt"; then + web_security_corpus_status=match +else + web_security_corpus_status=mismatch +fi + +if [ "$binary_status" = match ] && + [ "$staged_lib_status" = match ] && + [ "$generated_input_status" = match ] && + [ "$source_manifest_status" = match ] && + [ "$web_security_corpus_status" = match ]; then + status=match +else + status=mismatch +fi + +{ + echo "binary_status=$binary_status" + echo "staged_lib_status=$staged_lib_status" + echo "generated_input_status=$generated_input_status" + echo "source_manifest_status=$source_manifest_status" + echo "web_security_corpus_status=$web_security_corpus_status" + echo "jerbuild_binary_obj_dir=$obj_dir" + echo "jerbuild_binary_keep_obj_dir=1" + echo "jerboa_binary_canonicalize_wpo=$wpo_canonicalize" + echo "prepared_generated_lib_status=$prepared_artifact_status" + echo "discarded_warm_build=1" + echo "status=$status" +} > "$tmp_dir/report.txt" + +if [ "$binary_status" != match ]; then + diff -u "$tmp_dir/binary-first.sha256" "$tmp_dir/binary-second.sha256" \ + > "$tmp_dir/binary.diff" || true +fi + +if [ "$staged_lib_status" != match ]; then + diff -u "$tmp_dir/stage-first.sha256" "$tmp_dir/stage-second.sha256" \ + > "$tmp_dir/staged-libs.diff" || true +fi + +if [ "$generated_input_status" != match ]; then + diff -u "$tmp_dir/first-generated-inputs.sha256" "$tmp_dir/second-generated-inputs.sha256" \