OpenHikes is a single-developer project with no release branches. Only the
current main is supported; fixes land there and nowhere else.
Please do not open a public issue for a security problem.
Use GitHub's private vulnerability reporting for the repository, or email zsombor.rajki@gmail.com if that is unavailable.
Please include what you found, how to reproduce it, and what an attacker gets out of it. You should get an acknowledgement within a week. There is no bounty programme.
The app is local-first and there is no OpenHikes account, but that is not the same as having nothing on a server. Sharing a hike publishes it to a public CloudKit database this project owns and every user can read, and its security is entirely a hand-maintained configuration of permissions and indexes. That is the most interesting thing here to report on, so it is named first.
The rules are spelled out in OpenHikes/Community/Transport/CommunitySchema.swift, and
anything the deployed environment does that contradicts that file is worth
reporting:
CommunityHikeSubmission— what a hiker uploads._worldread,_icloudcreate,_creatorread only. If write came back to_creator, an author could swap the route or the photographs of an already-approved submission and have the app serve the replacement under the approved listing, unreviewed. That is the whole reason there are two record types.CommunityHikeSubmissioncarries no index at all,___recordIDincluded. Any way to enumerate that type is a finding: the absence of an index is the only thing keeping world-readable pending submissions from being listed, and it has gone wrong once already — a field reached the development environment carryingQUERYABLE SEARCHABLE SORTABLEby itself, because a schema promote can drop an index and downgrade a grant, not only add.CommunityHike— the published listing._worldread; create and write granted to a custom admin role — the reviewer's — and to nobody else. The app does have a method for it (CommunityTransporting.publish(_:)), and that is not the control: the grant is. A build calling it from an account outside the role gets a permission failure, exactly as a hand-rolled request would. A report that the method exists is not a finding; a report that a non-reviewer's call succeeded is.CommunitySubmissionNotice— the reviewer's queue._icloudcreate, and read and write granted to thereviewerrole and to nobody else. It carries a reference to whatever is waiting — a hike submission or a photo submission, exactly one of the two — and no other field. This is the one type in the schema that is indexed on purpose, and its read grant is what makes that safe — a query returns the rows the caller may read, which for everybody but the reviewer is none. Any read of this type by an account outside the role is a finding, because a notice names a pending submission and a name is all it takes to fetch one.CommunityHikeSubmissiongrantsWRITEtoreviewer, which is how a declined submission is deleted. Write reaching any other role —_creatorabove all — is the finding described in the first bullet.CommunityHike.authorIDis deliberately unindexed as well: blocking is applied on the device, so the field is never a predicate, and indexing it would let anybody enumerate one person's published hikes.CommunityPhotoSubmission— photographs offered to a hike, or to a waymarked OpenStreetMap route, that is already public. The same two-type shape as above and the same grants:_worldread,_icloudcreate,_creatorread only,WRITEto thereviewerrole and to nobody else, and no index at all. Every finding described forCommunityHikeSubmissionis a finding here word for word.CommunityPhotoContribution— the published set of contributed photographs._worldread; create and write granted to the reviewer's admin role alone, as onCommunityHike. It carries two queryable fields on purpose —listing, so opening a hike can ask which contributions are about it, andphotoSubmission, so a contributor's own device can ask whether theirs is live — and both queries need a name that is already public or that only the author holds. ItsauthorIDis unindexed for the reasonCommunityHike's is, and a query on it is the same enumeration finding.
Known and accepted, so a report of it is not new: an unreviewed submission is readable by anyone holding its record name — unguessable, and not enumerable, but a name rather than a permission. What would be new is a way to obtain such a name without being given it.
- Data reaching somewhere it should not — the mirrored (private) CloudKit
database, the
group.tappium.com.OpenHikesApp Group store the widget reads, the photo library, a tile provider's servers, or the paired Apple Watch, which is sent the titles and figures of the hiker's hikes, the geometry of one trail, and — while the watch app is open — the live figures of a recording the phone is making, and should be sent nothing else. - Anything exploitable through untrusted input: an imported GPX file, an
openhikes://deep link, a response from a tile or Overpass endpoint, a payload crossing the watch link (WatchLinkis the only decoder on either side), or the contents of a community record — which is text and assets another user uploaded. - A committed credential.
OpenHikes/Secrets.plistis gitignored and holds the optional Stadia and Thunderforest keys; a key that reached a commit is worth reporting even if the commit is old.
- Anything requiring physical access to an unlocked, already-trusted device.
- Map data and tiles served by OpenStreetMap, Stadia Maps, Thunderforest or Waymarked Trails — those are the providers' to secure. Report abuse to them.
- Vulnerabilities in Apple's frameworks. Report those to Apple.
Secret scanning and push protection are enabled on this repository, so a
recognised credential is blocked at push time rather than found afterwards.
Dependency updates come from Dependabot, and
CodeQL analyses Swift on every pull request that
touches it, on every push to main, and weekly.