codeberg4s is a client library. It holds an API token in memory, puts it in an
Authorization header, and builds request paths out of values a caller
supplies. Almost everything that could go wrong here falls into one of those
three sentences.
Email balyszyn@gmail.com. Do not open a public issue.
Include, as far as you can:
- what an attacker gains, and what they need already to get it;
- the affected version or commit;
- the operation id or method involved, if there is one;
- a minimal reproduction — Scala that compiles is ideal;
- a redacted transcript. If your evidence contains a real token, replace it with a placeholder and say so. Do not send working credentials; a report is not worth creating a second incident.
If the repository's host offers private vulnerability reporting (GitHub's "Report a vulnerability" button, for instance), that works too and reaches the same person.
There is no bug bounty and no published PGP key. If you need an encrypted channel, say so in a first message containing nothing sensitive and one will be arranged.
This project is maintained by one person, so these are commitments that can actually be kept rather than aspirational ones:
| Stage | Target |
|---|---|
| Acknowledgement that the report arrived | 7 days |
| An assessment — is it a vulnerability, and how severe | 14 days |
| A fix released, for anything credible and high severity | 30 days |
| Public disclosure | after a fix ships, or 90 days, whichever is first |
If you have not heard anything after 14 days, assume the mail went astray and send it again. Credit is given in the changelog unless you ask otherwise.
| Version | Supported |
|---|---|
0.1.0-SNAPSHOT (unreleased) |
this is the development line; fixes land on main |
Nothing has been released yet. Once 0.1.0 ships, the policy is: fixes go
to the latest patch of the current minor, and — while the major version is 0,
under the Early SemVer scheme this project declares — the previous minor gets a
backport only if the fix is small and the release is recent. Pinning an old
0.x is not a supported way to avoid an upgrade.
Because the library is published as five artifacts under one version, a security fix is released across all five together even when only one changed.
Anything reachable through the published API of the five artifacts.
The library's central promise is that a token cannot escape through a
diagnostic path. ApiToken renders as *** in toString and in string
interpolation, reveal is the only accessor, CallContext holds a URI that
was redacted before the context was built, and CodebergException's message is
CodebergError.describe, which is assembled only from that redacted context
and from server-supplied text. There are tests asserting each of these, and
property suites over the rendering paths.
Any way to get credential material out through one of these is a vulnerability, not a bug, however unlikely the path:
- a token, password or basic-auth header appearing in a
toString, adescribe, an exception message, a stack trace or a telemetry callback; - a credential surviving in a URI, a query parameter or a header that reaches
CallContext, a log line or an error body; - a redaction that can be defeated by a crafted server response — an error body echoed back into a message, for instance;
- a credential written to disk, to a temporary file, or into a coverage or debugging artifact.
Report these privately even if the path looks theoretical.
Owners, repository names, branch names, labels, paths and page sizes are opaque
types with Either-returning smart constructors, and those constructors are a
security boundary, not a convenience. A value that escapes validation and
changes the request path — path traversal out of /repos/{owner}/{repo}/…,
a smuggled query parameter, a CRLF injected into a header — is in scope.
- Anything that would cause a request to be sent to a host other than the
configured
BaseUri, including redirect handling. - Anything that weakens TLS verification.
- A retry of a non-idempotent operation.
POSTandPATCHthat create or edit are deliberately never retried; a change that makes one retryable can file the same issue twice or double a payment-like side effect.
- A crafted 2xx payload that causes unbounded memory growth, non-terminating
decoding, or an exception that escapes the
CodebergErrorchannel. Note the documented guard: every response is read under a byte bound —CodebergConfig.maxResponseBodyBytes, 16 MiB by default, andmaxDownloadBodyBytes, 50 MiB, for the two archive downloads — and a body that passes it is abandoned asTransportCause.ResponseTooLargerather than read to the end. A payload that exhausts memory while staying inside that bound is a report. - A
Linkheader that drives a pagination walk into an infinite loop. Note the documented guard: a walk stops when the server stops offering a next page, and callers are told to guard on an empty page as well.
A known vulnerability in sttp client4 or upickle that this library exposes.
The dependency surface is deliberately two libraries; codeberg4s-domain has
none at all.
- Vulnerabilities in Forgejo, Gitea or codeberg.org. Report those upstream. Their trackers are the right place; this library only calls them.
- Anything requiring the attacker to already run code in your process. A heap dump contains the token; that is what holding a credential in memory means, and no library-level mitigation changes it.
- Configuration you chose. Pointing
BaseUriat a host you do not trust, loggingtoken.revealyourself, or committing a token to a repository. - Rate limiting and denial of service against a remote instance caused by
your own call volume. The retry policy is bounded and honours
Retry-After; driving a walk over ten thousand pages is a decision the caller makes. - Known limitations already documented. Two operations —
DownloadActionArtifactandrepoGetActionRunLogs, reachable asclient.downloads— hold the whole archive in memory; the library does not stream, and attachment streaming above 50 MB is explicitly out of scope for v1. Those two are bounded byCodebergConfig.maxDownloadBodyBytes, which defaults to 50 MiB for exactly that reason; raising the setting yourself and then running out of heap is your configuration, not a vulnerability. - Missing hardening with no exploit path, such as the absence of certificate pinning.
Stated so you know what to test against, not as a claim of safety:
-Werrorwith-Wunused:all,-Wvalue-discardand-Wnonunit-statement; Scalafix bansnull,throw,return,var, casts and unsafeOptionaccess outright.- No recoverable failure is thrown: failures are
CodebergError, and a barenew Exceptionin production code fails./verify.sh. - Redaction is tested directly, including property suites asserting that no rendering path emits a credential.
- Every request carries a response-body byte bound, so an instance cannot answer a call with as many bytes as it likes. Exceeding it is not retried: repeating the call would download the oversized body once per attempt.
- Two dependencies, both widely used, both pinned in
build.mill. - Released artifacts are PGP-signed and built by
.github/workflows/release.ymlfrom a tagged commit, after the same./verify.shevery push runs. Signing keys and Sonatype tokens live only in repository secrets, and are passed to Mill as environment variables so they never reach a process listing or a build log. SeeRELEASING.md.
Maven Central is immutable: a published version cannot be altered or withdrawn.
The response is to release a fixed version immediately, mark the bad one in
CHANGELOG.md, and — if a credential was exposed — rotate it first, because
rotation is the only mitigation that does not depend on anyone upgrading.
RELEASING.md § "When a release is bad" has the procedure.