Skip to content

Commit bf1846e

Browse files
committed
fix: harden renderer defaults and add audit regression coverage
- prefer packaged binaries and require explicit PATH opt-in for system fallback - log renderer stderr server-side while masking client-facing example server errors - enforce safer map/style limits, Web Mercator latitude bounds, and temp-style reuse checks - move geopy to an optional extra, pin toolchain inputs, and restore CI pip-audit scanning - add regression tests and refresh ISSUES.md with the current audit findings
1 parent 7811e58 commit bf1846e

20 files changed

Lines changed: 548 additions & 192 deletions

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
- name: Install Rust
4040
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
4141
with:
42-
toolchain: 1.90.0
42+
toolchain: 1.94.1
4343
components: clippy
4444

4545
- name: Run clippy
@@ -88,7 +88,7 @@ jobs:
8888
run: just ci-setup
8989

9090
- name: Scan locked Python environment
91-
run: uv run pip-audit
91+
run: uv run --frozen --with pip-audit pip-audit
9292

9393
build-binary:
9494
name: Build binary ${{ matrix.platform }}

.github/workflows/release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
- name: Install Rust toolchain
3838
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
3939
with:
40-
toolchain: 1.90.0
40+
toolchain: 1.94.1
4141

4242
- name: Setup tools with mise
4343
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
@@ -72,7 +72,7 @@ jobs:
7272
- name: Install Rust toolchain
7373
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
7474
with:
75-
toolchain: 1.90.0
75+
toolchain: 1.94.1
7676

7777
- name: Setup tools with mise
7878
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
@@ -110,7 +110,7 @@ jobs:
110110
- name: Install Rust toolchain
111111
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
112112
with:
113-
toolchain: 1.90.0
113+
toolchain: 1.94.1
114114

115115
- name: Setup tools with mise
116116
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4

.mise.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tools]
2-
python = "3.12"
3-
rust = "1.90.0"
4-
just = "1.46.0"
2+
python = "3.14.3"
3+
rust = "1.94.1"
4+
just = "1.48.1"
55
uv = "0.11.3"
66

77
[env]

ISSUES.md

Lines changed: 62 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,95 @@
11
# Audit Issues
22

3-
- Date: 2026-04-02
4-
- Commit: `2ad80ce49f12`
5-
- Codebase summary (`sloc`): 49 files, 2,470 code LOC, 1,290 documentation LOC, 5,670 total lines; main languages: Python 1,654 LOC, YAML 353 LOC, Rust 249 LOC.
6-
- Scope: Python wrapper (`mlnative/`), Rust renderer (`rust/`), example servers (`examples/`), CI/release workflows (`.github/workflows/`), and helper scripts (`scripts/`).
3+
- Date: 2026-04-03
4+
- Commit: `9bd2eac59da5`
5+
- Codebase summary (`sloc`): 50 files, 2,696 code LOC, 1,271 documentation LOC, 6,004 total lines; main languages: Python 1,869 LOC, YAML 353 LOC, Rust 259 LOC.
6+
- Scope: Python wrapper (`mlnative/`), Rust renderer (`rust/`), example servers (`examples/`), CI/release workflows (`.github/workflows/`), packaging/build helpers, and test/runtime container files.
77
- Audit references: [OWASP ASVS 5.0](https://owasp.org/www-project-application-security-verification-standard/) with emphasis on [V15 Secure Coding and Architecture](https://raw.githubusercontent.com/OWASP/ASVS/v5.0.0/5.0/en/0x24-V15-Secure-Coding-and-Architecture.md), [V16 Security Logging and Error Handling](https://raw.githubusercontent.com/OWASP/ASVS/v5.0.0/5.0/en/0x25-V16-Security-Logging-and-Error-Handling.md), and [grugbrain.dev](https://grugbrain.dev/).
8+
- Local dependency lookup: Renovate dry-run succeeded on 2026-04-03; the only actionable delta surfaced was `actions/attest-build-provenance` digest `b3e506e8c389afc651c5bacf2b8f2a1ea0557215``a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32`.
89

910
## Prioritized findings
1011

11-
### 1) User-controlled `style` reaches network and filesystem loaders
12-
- Location: `examples/fastapi_server.py:42-76`, `examples/web_test_server.py:43-126`, `mlnative/map.py:118-136`, `rust/src/main.rs:82-97`
12+
### 1) Reused temp style file is truncated but not rewound before rewrite
13+
- Location: `rust/src/main.rs:89-99`, `tests/test_issue_resolutions.py:138-145`
1314
- Category: security
14-
- Reference: ASVS `v5.0.0-15.3.2`, `v5.0.0-15.2.5`
15-
- Recommendation: Do not accept arbitrary style URLs/paths from request data. Expose style IDs from an allowlist, reject local paths/`file://` in server contexts, and keep redirect behavior explicit.
16-
- Status: open
17-
18-
### 2) Example render endpoints are expensive, unauthenticated, and bind publicly by default
19-
- Location: `examples/fastapi_server.py:33-101`, `examples/web_test_server.py:96-155`
20-
- Category: security
21-
- Reference: ASVS `v5.0.0-15.1.3`, `v5.0.0-15.2.2`
22-
- Recommendation: Keep demo servers on `127.0.0.1` by default and add auth, rate limits, concurrency caps, request budgets, and caching before any non-local exposure.
23-
- Status: open
24-
25-
### 3) Errors are leaked to clients while renderer stderr is discarded
26-
- Location: `examples/fastapi_server.py:82-83`, `examples/web_test_server.py:132-140`, `mlnative/_bridge.py:114-120`
27-
- Category: security
28-
- Reference: ASVS `v5.0.0-16.5.1`, `v5.0.0-16.3.4`
29-
- Recommendation: Return generic client errors, log structured exceptions server-side, and retain renderer stderr behind a debug/logging path instead of dropping it.
15+
- Reference: ASVS `v5.0.0-16.5.3`
16+
- Recommendation: Seek to offset 0 before rewriting the reused `NamedTempFile` (or recreate it), then add a regression test that reloads a JSON style twice. As written, repeat `reload_style()` / `set_geojson()` calls can produce a NUL-prefixed style file and break rendering.
3017
- Status: open
3118

32-
### 4) Native binary resolution still trusts the first `PATH` hit
33-
- Location: `mlnative/_bridge.py:70-92`
19+
### 2) Renderer timeouts can desynchronize the daemon protocol and hand late responses to the wrong caller
20+
- Location: `mlnative/_bridge.py:181-225`, `mlnative/_bridge.py:277-306`
3421
- Category: security
35-
- Reference: ASVS `v5.0.0-15.2.4`, `v5.0.0-15.2.5`
36-
- Recommendation: Prefer packaged binaries only by default. If overrides are needed, gate them behind explicit opt-in and verify ownership/hash/signature.
22+
- Reference: ASVS `v5.0.0-15.4.1`, `v5.0.0-16.5.3`
23+
- Recommendation: Treat a timeout as terminal for that daemon instance, or add request IDs and response matching. Today a timed-out command leaves its eventual response in the shared queue, so the next command can consume stale data.
3724
- Status: open
3825

39-
### 5) Binary download helper executes unverified release artifacts
40-
- Location: `scripts/download-binary.py:36-74`
26+
### 3) Example servers still let request data choose arbitrary remote URLs and local style files
27+
- Location: `examples/fastapi_server.py:42-75`, `examples/web_test_server.py:44-129`, `examples/templates/test_form.html:61-63`, `mlnative/map.py:122-139`, `rust/src/main.rs:82-102`
4128
- Category: security
42-
- Reference: ASVS `v5.0.0-15.2.4`
43-
- Recommendation: Verify checksums/signatures before `chmod +x`, or remove the helper and rely on trusted package distribution only.
29+
- Reference: ASVS `v5.0.0-15.3.2`, `v5.0.0-15.2.5`
30+
- Recommendation: In server contexts, expose style IDs from an allowlist instead of raw `style` strings, reject local paths / `file://`, and run the renderer with explicit egress restrictions if any untrusted input remains.
4431
- Status: open
4532

46-
### 6) CI dependency scanning is currently broken; maintenance updates are also pending
47-
- Location: `.github/workflows/ci.yml:77-91`, `pyproject.toml:23-40`, `.mise.toml:1-5`, `rust/Cargo.toml:9-15`
33+
### 4) Render endpoints are expensive but ship without auth, quotas, caching, or concurrency budgets
34+
- Location: `examples/fastapi_server.py:41-79`, `examples/web_test_server.py:112-129`, `examples/production_deployment.py:38-67`
4835
- Category: security
49-
- Reference: ASVS `v5.0.0-15.1.1`, `v5.0.0-15.2.1`
50-
- Recommendation: Make the scan runnable in CI (`uvx pip-audit`, or add `pip-audit` to the CI environment) and review the local Renovate lookup deltas, notably `tempfile 3.23→3.27`, `rust 1.90.0→1.94.1`, `just 1.46.0→1.48.1`, `python 3.12→3.14.3`, and the `actions/attest-build-provenance` digest refresh.
36+
- Reference: ASVS `v5.0.0-15.1.3`, `v5.0.0-15.2.2`
37+
- Recommendation: Before any non-local exposure, add authentication, per-client rate limits, worker/concurrency caps, cache hot responses, and explicit time/size budgets.
5138
- Status: open
5239

53-
### 7) `fit_bounds()` accepts `-90` latitude then falls into a raw `math domain error`
54-
- Location: `mlnative/map.py:317-344`
55-
- Category: security
56-
- Reference: ASVS `v5.0.0-16.5.3`
57-
- Recommendation: Reject/clamp to Web Mercator-safe latitude bounds before projection and convert failures to `MlnativeError` consistently.
40+
### 5) The “production-ready” pool example can block forever and its health check performs a full remote render
41+
- Location: `examples/production_deployment.py:58-67`, `examples/production_deployment.py:99-104`
42+
- Category: performance
43+
- Reference: ASVS `v5.0.0-15.1.3`; grugbrain.dev
44+
- Recommendation: Use bounded wait times on `Queue.get()`, return overload errors instead of hanging, and split cheap liveness checks from heavyweight render/dependency checks.
5845
- Status: open
5946

60-
### 8) Batch rendering has no request cap and buffers all PNGs in memory at once
61-
- Location: `mlnative/map.py:229-278`, `mlnative/_bridge.py:166-176`, `rust/src/main.rs:287-327`
47+
### 6) Batch rendering still buffers the full response set in memory twice
48+
- Location: `rust/src/main.rs:301-337`, `mlnative/_bridge.py:197-210`, `mlnative/_bridge.py:338-364`, `mlnative/map.py:238-246`
6249
- Category: performance
6350
- Reference: ASVS `v5.0.0-15.1.3`, `v5.0.0-15.2.2`
64-
- Recommendation: Add view-count/output-size limits and prefer streaming or chunked framing over whole-batch buffering for large jobs.
51+
- Recommendation: Keep the view/pixel caps, but move to per-image streaming or chunked framing. The Rust side accumulates every PNG before send, and the Python side then reads the combined payload and copies each slice again.
6552
- Status: open
6653

67-
### 9) JSON style reloads leak temp files for the daemon lifetime
68-
- Location: `rust/src/main.rs:65-94`, `rust/src/main.rs:140-146`
69-
- Category: performance
70-
- Reference: ASVS `v5.0.0-15.1.3`
71-
- Recommendation: Reuse a single temp style file or drop old `NamedTempFile`s before pushing new ones; repeated `reload_style()`/`set_geojson()` currently grows file descriptors and temp storage monotonically.
54+
### 7) `set_geojson()` still reloads the entire style document for every source update
55+
- Location: `mlnative/map.py:438-482`
56+
- Category: complexity
57+
- Reference: ASVS `v5.0.0-15.1.3`; grugbrain.dev (“complexity very, very bad”)
58+
- Recommendation: Add source-level mutation in Rust or separate immutable style from mutable data. The current full-style rewrite is a simple API, but it makes frequent data updates expensive and harder to reason about.
7259
- Status: open
7360

74-
### 10) `set_geojson()` reloads the entire style for each source update
75-
- Location: `mlnative/map.py:413-451`
61+
### 8) Release builds create “platform wheels” by renaming a pure `py3-none-any` wheel
62+
- Location: `Justfile:96-114`, `.github/workflows/release.yml:83-90`, `pyproject.toml:54-57`
7663
- Category: complexity
77-
- Reference: ASVS `v5.0.0-15.1.3`; grugbrain.dev (“complexity very, very bad”)
78-
- Recommendation: Add source-level mutation in Rust, or clearly document/cap the full-style reload cost for update-heavy workloads.
64+
- Reference: ASVS `v5.0.0-15.1.2`; grugbrain.dev
65+
- Recommendation: Use a real platform-wheel pipeline (`cibuildwheel`, `auditwheel`, `delocate`, `maturin`, or equivalent) or rewrite embedded wheel metadata consistently. The current release job renames the filename only; a local build still emits `Root-Is-Purelib: true` and `Tag: py3-none-any`.
7966
- Status: open
8067

81-
### 11) `geopy` is a required runtime dependency even though the library core does not use it
82-
- Location: `pyproject.toml:24-29`, `README.md:21-25`, `examples/address_rendering.py:1-26`
68+
### 9) CI integration tests depend on live third-party network and remote tile/style services
69+
- Location: `tests/test_render.py:34-39`, `tests/test_render.py:85-95`, `.github/workflows/ci.yml:138-222`, `docs/CI.md:20-33`, `docs/CI.md:128-134`
8370
- Category: complexity
84-
- Reference: ASVS `v5.0.0-15.2.3`; grugbrain.dev
85-
- Recommendation: Move geocoding dependencies into an example/extra (for example `geo` or `examples`) so the core renderer ships with a smaller attack and maintenance surface.
71+
- Reference: ASVS `v5.0.0-15.1.3`; grugbrain.dev
72+
- Recommendation: Move CI to local fixtures, a pinned test style, or a local mock tile server. Current smoke/integration coverage is useful, but it is also coupled to OpenFreeMap availability and network latency.
73+
- Status: open
74+
75+
### 10) Test container still bootstraps toolchain code via `curl | sh`
76+
- Location: `Dockerfile.test:21`
77+
- Category: security
78+
- Reference: ASVS `v5.0.0-15.2.4`
79+
- Recommendation: Pin a checksum or versioned installer artifact, or use a trusted package source instead of piping a live script directly into `sh`.
80+
- Status: open
81+
82+
### 11) Release provenance action digest is already stale per local Renovate lookup
83+
- Location: `.github/workflows/release.yml:161-163`, `.github/workflows/release.yml:200-202`, `renovate.json:1-3`
84+
- Category: security
85+
- Reference: ASVS `v5.0.0-15.1.1`, `v5.0.0-15.2.1`
86+
- Recommendation: Refresh `actions/attest-build-provenance` to digest `a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32` and keep digest refreshes within the repo’s documented remediation window.
8687
- Status: open
8788

8889
## Resolved log
89-
- Previously flagged workflow action drift is resolved in the current tree: committed workflow YAML is digest-pinned (`.github/workflows/ci.yml`, `.github/workflows/release.yml`).
90-
- Previously flagged predictable temp style filenames are resolved: the Rust daemon now uses `tempfile::NamedTempFile` (`rust/src/main.rs`).
91-
- Previously flagged unit-test selection drift is resolved: `just test-unit` now excludes only `integration` tests (`Justfile:34-36`).
92-
- Previously flagged floating tool versions are resolved: `.mise.toml` now pins Python, Rust, Just, and uv (`.mise.toml:1-5`).
93-
- Previously flagged dead vendored Node renderer surface is resolved in the current tree: the tracked JS renderer/vendor files are gone from `mlnative/`.
94-
- Previously flagged Rust patch lag is resolved: `maplibre_native` and `image` are already at the looked-up current versions in `rust/Cargo.toml`.
95-
- Previously flagged PNG base64 transport/thread-per-command issues are resolved: the current bridge uses a persistent reader thread and raw binary payload framing (`mlnative/_bridge.py`, `rust/src/main.rs`).
90+
- Example servers now bind to loopback by default instead of `0.0.0.0` (`examples/fastapi_server.py:112`, `examples/web_test_server.py:165`).
91+
- Native binary lookup now prefers packaged binaries and requires explicit opt-in for `PATH` fallback (`mlnative/_bridge.py:101-110`, `tests/test_bridge.py:33-57`).
92+
- The old binary download helper now fails closed instead of downloading and executing an unverified artifact (`scripts/download-binary.py:36-53`).
93+
- CI security scanning is wired back in via `uv run --frozen --with pip-audit pip-audit` (`.github/workflows/ci.yml`).
94+
- `fit_bounds()` now enforces Web Mercator-safe latitude bounds and fails with `MlnativeError` instead of raw projection errors (`mlnative/map.py:333-352`).
95+
- `geopy` is no longer a core runtime dependency; it lives in the optional `geo` extra (`pyproject.toml:26-33`).

README.md

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,24 @@ pip install mlnative
1818

1919
```python
2020
from mlnative import Map
21-
from geopy.geocoders import ArcGIS
22-
23-
# Geocode an address
24-
geolocator = ArcGIS()
25-
location = geolocator.geocode("San Francisco")
2621

27-
# Render map at that location
2822
with Map(512, 512) as m:
29-
png = m.render(
30-
center=[location.longitude, location.latitude],
31-
zoom=12
32-
)
23+
png = m.render(center=[-122.4194, 37.7749], zoom=12)
3324
open("map.png", "wb").write(png)
3425
```
3526

27+
For geocoding examples, install the optional extra:
28+
29+
```bash
30+
pip install 'mlnative[geo]'
31+
```
32+
3633
## Features
3734

3835
- **Zero config** - Works out of the box with OpenFreeMap tiles
3936
- **HiDPI support** - `pixel_ratio=2` for sharp retina displays
4037
- **Batch rendering** - Efficiently render hundreds of maps
41-
- **Address geocoding** - Built-in support via geopy
38+
- **Optional geocoding extra** - Use `mlnative[geo]` for address lookup examples
4239
- **Custom markers** - Add GeoJSON points, lines, polygons
4340

4441
## Screenshots
@@ -68,11 +65,11 @@ Both images show the exact same geographic area. The 2x version has 4x more pixe
6865

6966
## Examples
7067

71-
### Render from address
68+
### Render from address (optional `geo` extra)
7269

7370
```python
74-
from mlnative import Map
7571
from geopy.geocoders import ArcGIS
72+
from mlnative import Map
7673

7774
geolocator = ArcGIS()
7875
location = geolocator.geocode("Sydney Opera House")
@@ -84,6 +81,9 @@ with Map(512, 512) as m:
8481
)
8582
```
8683

84+
Install first with `pip install 'mlnative[geo]'`.
85+
86+
8787
### Fit bounds to show area
8888

8989
```python
@@ -109,7 +109,7 @@ with Map(800, 600) as m:
109109
png = m.render(center=center, zoom=zoom)
110110
```
111111

112-
### Batch render multiple cities
112+
### Batch render multiple cities (optional `geo` extra)
113113

114114
```python
115115
from geopy.geocoders import ArcGIS
@@ -136,24 +136,15 @@ with Map(512, 512) as m:
136136
Use `pixel_ratio` to render high-resolution images for crisp display on retina/HiDPI screens.
137137

138138
```python
139-
from geopy.geocoders import ArcGIS
140-
141-
geolocator = ArcGIS()
142-
location = geolocator.geocode("Paris")
139+
center = [2.3522, 48.8566] # Paris
143140

144141
# Standard display (1x) - 512x512 image
145142
with Map(512, 512, pixel_ratio=1) as m:
146-
png = m.render(
147-
center=[location.longitude, location.latitude],
148-
zoom=13
149-
)
143+
png = m.render(center=center, zoom=13)
150144

151145
# Retina/HiDPI display (2x) - 1024x1024 image
152146
with Map(512, 512, pixel_ratio=2) as m:
153-
png = m.render(
154-
center=[location.longitude, location.latitude],
155-
zoom=13
156-
)
147+
png = m.render(center=center, zoom=13)
157148
# Same geographic area, but text appears sharper
158149
```
159150

@@ -196,11 +187,12 @@ views = [
196187

197188
# Per-view GeoJSON updates are not supported here. Use set_geojson()
198189
# and render() in a loop when each image needs different source data.
190+
# Large batches are capped to keep memory use predictable.
199191
```
200192

201193
### fit_bounds(bounds, padding=0, max_zoom=24)
202194

203-
Calculate center/zoom to fit bounding box.
195+
Calculate center/zoom to fit bounding box. Bounds must stay within Web Mercator latitude limits (about ±85.0511°).
204196

205197
```python
206198
center, zoom = m.fit_bounds((xmin, ymin, xmax, ymax))
@@ -210,6 +202,7 @@ png = m.render(center=center, zoom=zoom)
210202
### set_geojson(source_id, geojson)
211203

212204
Update GeoJSON source in style (requires dict style, not URL).
205+
Each update reloads the full style in the current backend, so keep source payloads modest.
213206

214207
```python
215208
m.set_geojson("markers", {"type": "FeatureCollection", "features": [...]})

examples/address_rendering.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
maps from addresses without manually looking up coordinates.
55
"""
66

7-
from geopy.geocoders import ArcGIS
7+
try:
8+
from geopy.geocoders import ArcGIS
9+
except ImportError as e:
10+
raise SystemExit("Install the optional geo extra to run this example: pip install 'mlnative[geo]'") from e
811

912
from mlnative import Map
1013

examples/basic.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
Renders maps using addresses instead of hardcoded coordinates.
66
"""
77

8-
from geopy.geocoders import ArcGIS
8+
try:
9+
from geopy.geocoders import ArcGIS
10+
except ImportError as e:
11+
raise SystemExit("Install the optional geo extra to run this example: pip install 'mlnative[geo]'") from e
912

1013
from mlnative import Map
1114

0 commit comments

Comments
 (0)