Address review feedback on PR #2044.
safehttp.Transport():
* Clone http.DefaultTransport instead of building a bare &http.Transport{} so
Proxy, ForceAttemptHTTP2, MaxIdleConns, IdleConnTimeout, TLSHandshakeTimeout
and ExpectContinueTimeout are inherited (the bare struct loses them all).
Verified by new TestTransport_PreservesDefaultTransportSettings.
* TestTransport_AllowsPublic: bound the dial of TEST-NET-3 with a 100ms
context so the test does not depend on real-world routing of 203.0.113.0/24,
and drop the dead dialer var.
proxy/image.go:
* Document Image.Transport contract: nil installs safehttp.Transport (SSRF-safe);
caller-supplied transport is the caller's responsibility.
* Replace the misleading "SSRF mitigated by safehttp.Transport" nolint comments
with one that points at the documented contract above.
Address all golangci-lint v2.10.1 (CI's version) findings:
* Add http.MaxBytesReader hard cap to ParseMultipartForm sites in
rest_private.savePictureCtrl (32MB) and api/migrator (256MB) — fixes
G120 by bounding total request body before form parsing.
* Suppress G70x in CLI subcommands cmd/{backup,cleanup,import,remap}.go:
all four issue HTTP requests against operator-supplied RemarkURL/CLI
flags, never user input. Each suppression carries a one-line reason.
* Suppress G122 in image fs_store cleanup walk: staging directory tree
is server-only, no untrusted symlinks land there.
CI's golangci-lint v2.10.1 (newer rule set than my local 2.11.4) flags
four G70x cases the previous run missed. All are false positives:
backup.go and cleanup.go drive HTTP requests against the operator's own
RemarkURL from CLI flags (not user input); migrator.go removes a temp
file whose name was returned by os.CreateTemp (server-controlled). Add
targeted //nolint:gosec comments naming the reason at each site.
Commit aca0cff3 silenced the path-traversal, SSRF and XSS taint rules
project-wide as "false positives" while fixing image-proxy SSRF. With
the path-traversal and TitleExtractor SSRF gaps now closed, restore the
rules so future regressions get flagged. The four genuine false positives
that remain (image proxy http.NewRequest, QR png Write, two RSS XML
Writes) get individual //nolint:gosec comments naming the reason.
The image proxy got an ssrfSafeTransport in commit aca0cff3 that resolves
DNS first, blocks any IP in private/reserved CIDRs, then dials by IP to
defeat DNS rebinding. The TitleExtractor used to construct comments'
PostTitle from Locator.URL — a user-supplied field — was missed by that
fix and kept using http.DefaultTransport. The hostname allowlist there
checks the parsed URL host but never the IP it resolves to, so a domain
suffix-matching an allowed host (or 127.0.0.1 itself when AllowedHosts
is empty) reaches the metadata service or any other internal endpoint.
The same gosec rule (G704) was excluded globally in .golangci.yml as part
of aca0cff3, so this gap was not caught by the linter either.
Extract the transport into a new safehttp package so it lives in one
place and can be reused, then pass safehttp.Transport() into the
TitleExtractor's http.Client at construction (cmd/server.go). The image
proxy switches to safehttp.Transport() too — same behaviour, no longer
duplicated.
Reproduction in title_test.go uses the production-style client to hit
an httptest.Server (always 127.0.0.1) and asserts the dialer refuses
even though "127.0.0.1" is in the allowed-domains list. A control case
shows the same setup without safehttp.Transport returns the page —
making the original vulnerability explicit.
Address PR #2045 review (umputun):
* The //nolint:gosec on telegramQrCtrl's w.Write(png) was byte-identical
to the same line in #2044 (gosec-rule restoration). Drop it here so
the two PRs do not conflict; #2044 owns it.
* `seg == ".."` in safePictureSegment was already covered by the
strings.Contains(seg, "..") check two lines down — trim and add an
inline comment so the cover-by-superset is explicit.
Address PR #2045 review feedback (Copilot #2045-1). The previous
safePictureSegment allowed CR/LF/TAB through, so a request such as
GET /api/v1/picture/dev%0Auser/abc.png would inject literal newlines
into the access log line ("GET - /api/v1/picture/dev\nuser/abc.png ...")
— a log-forgery primitive against any operator parsing those logs.
Reject any unicode.IsControl rune in either segment (NUL was already
caught via strings.ContainsAny). New TestRest_LoadPictureRejectsControlCharsInSegment
covers LF, CR, TAB, NUL across both segments.
The unauthenticated GET /api/v1/picture/{user}/{id} handler concatenated the
two URL params verbatim into a filesystem path via path.Join, so a request
like /api/v1/picture/../remark.db resolved to <base>/../remark.db, escaping
the image directory. With Partitions=0 (a documented option) this is a
direct arbitrary-file read; with the default Partitions=100 the constructed
path lands in a CRC-derived subdirectory but the server still leaks the
internal filesystem path back to the unauthenticated caller via the JSON
error body — confirmed against demo.remark42.com (master-80c12a3) which
returned `stat /var/folders/.../staging/.../remark.db` for `..` requests.
Validate both URL segments via safePictureSegment (no traversal markers,
no path separators, no NULs) at the handler entry, and replace the raw
storage error with a generic "image not found" response. The original
error is logged for operators.
Reproduction test asserts that ../remark.db, foo/..%2Fremark.db and
%2E%2E/remark.db all return 400 with no internal path leaked.
matchSiteID guarded most authenticated and admin routes with
`if siteID != "" && user.SiteID != siteID`. Dropping the ?site= query
parameter made the check no-op and any authenticated user passed the
middleware. Downstream handlers fell back to reading site from the JSON
body or just used the empty string, so on email/telegram subscribe
endpoints (which read site from body) a user authenticated to siteA
could perform actions targeting siteB without the cross-site guard
ever firing.
Require ?site= to be present and to match user.SiteID. Body-only site
flows are still supported provided the URL also carries the matching
?site= — both must agree, which removes the bypass and keeps the
declared site visible to the middleware.
Reproduction TestRest_matchSiteID enumerates four cases (matching,
mismatched, missing, empty). Existing test calls that relied on the
implicit pass had to add ?site=remark42 to the URL: the addComment
helper now derives the param from c.Locator.SiteID, picture upload
URL gets the param explicitly, and the email/telegram subscribe table
adds it to every endpoint. The negative cases that previously asserted
StatusBadRequest from the handler now correctly assert StatusForbidden
from the middleware.
Apply go fix ./... analysers (Go 1.26) across backend and examples:
- interface{} → any (type alias, no behaviour change)
- for i := 0; i < N; i++ → for range N / for i := range N
- slices.Contains / slices.ContainsFunc replacing manual loops
- strings.SplitSeq replacing strings.Split in range (avoids allocation)
- strings.CutPrefix replacing HasPrefix+TrimPrefix
- min() replacing manual if/else
- fmt.Appendf replacing []byte(fmt.Sprintf(...))
- strings.Builder replacing string += concatenation
- wg.Go(func(){}) replacing wg.Add(1)/go/wg.Done() pattern
- removed redundant ii := i loop variable copies (unnecessary since Go 1.22)
omitempty on struct-typed JSON fields: go fix removed omitempty from
struct-typed fields (time.Time, PostInfo, UserDetailEntry) because
encoding/json's omitempty never applied to struct types — it was always
a no-op. Kept as bare tags (no omitzero replacement) to preserve the
existing serialisation behaviour.
Replace strings.Split(RemoteAddr, ":") with net.SplitHostPort for correct
IPv6 address extraction in vote deduplication and comment IP tracking.
Harden image proxy: add SSRF-safe transport blocking private/reserved IPs
at connection time with DNS rebinding protection, sanitize error messages
to prevent information leakage, add response size limit via io.LimitReader.
Fix shadowed error variables in BlockedUsers, SetTitle, and Delete methods.
Exclude gosec taint analysis false positives at linter config level.
Add two missing security headers to the existing securityHeadersMiddleware:
- X-Content-Type-Options: nosniff — prevents browsers from MIME-sniffing
responses away from the declared Content-Type, stopping e.g. a
user-uploaded image from being reinterpreted as executable HTML/JS
- Referrer-Policy: strict-origin-when-cross-origin — limits URL information
leaked in the Referer header on cross-origin requests to just the origin
(no path), and sends nothing at all on HTTPS-to-HTTP downgrades
Replace WriteHeader() + RenderJSON() pattern with EncodeJSON() which
properly sets Content-Type header before writing status code. The
previous pattern caused Content-Type to default to text/plain instead
of application/json, breaking frontend JSON parsing.
Fixes#1979
Replace go-chi/render with go-pkgz/rest for JSON responses and custom
helpers for HTML/plain text responses.
Key changes:
- Replace render.JSON/render.Status with rest.RenderJSON and explicit
w.WriteHeader() calls
- Replace render.DecodeJSON with json.NewDecoder().Decode()
- Add SendErrorJSON helper that sets Content-Type header before
WriteHeader (required since rest.RenderJSON can't set headers after
WriteHeader is called)
- Add HTMLResponse and PlainTextResponse helpers
Fix export double-execution in migrator.go:
The original code called Export twice - once to io.Discard to check for
errors, then again to actually write. This was wasteful and had a race
condition risk. Now file mode buffers to memory first for atomic
success/failure, while stream mode writes directly with proper error
handling.
`format=tree` pagination provides top-level comments with all replies
and returns the last top-level comment as `last_comment` to be used
as `offset` for the next page. If comments and replies overflow
the limit, the one stepping out of the limit will not be returned.
If the first comment and its replies after the given offset overflow
the limit, it will be returned with all the replies.
`format=plain` pagination works by providing all comments and returning
the last comment as `last_comment` to be used as `offset`
for the next page.
Change the default img-src value to "*" and sets it to "'self'" when
image proxy is enabled. The previous state was inversion of this logic
which was wrong.
`Content-Security-Policy` now restricts resource loading and execution
to enhance security:
- `default-src 'none'`: Disallow all resource loading by default.
- `base-uri 'none'`: Prevents the use of `<base>` tag to change the
base URL for relative URLs.
- `form-action 'none'`: Disallows form submissions.
- `connect-src 'self'`: Restricts the origins that can be connected to
(via XHR, WebSockets, etc.) to the same origin.
- `frame-src 'self'`: Restricts the origins that can be embedded using
`<frame>` and `<iframe>` to the same origin (for `/web/` demo
endpoint).
- `frame-ancestors %s;`: Specifies the origins that are allowed to
embed this content in a frame. If no specific origins are allowed, it
defaults to `*` (any origin). This enhances security by controlling
which sites can embed your content.
- `img-src 'self'`: Allows images to be loaded only from the same
origin. If `imageProxyEnabled` is true, allows images from any origin
(`*`).
- `script-src 'self' 'unsafe-inline'`: Allows scripts to be loaded and
executed only from the same origin and allows inline scripts.
- `style-src 'self' 'unsafe-inline'`: Allows styles to be loaded and
applied only from the same origin and allows inline styles.
- `font-src data:`: Allows fonts to be loaded from data URIs.
- `object-src 'none'`: Disallows the use of `<object>`, `<embed>`, and
`<applet>` tags.
`Permissions-Policy` now restricts the use of certain browser features
which we don't use to enhance user privacy and security:
- `accelerometer=()`: Disables the use of the accelerometer sensor.
- `autoplay=()`: Disables automatic playback of media.
- `camera=()`: Disables the use of the camera.
- `cross-origin-isolated=()`: Disallows the page from being treated as
cross-origin isolated.
- `display-capture=()`: Disables the ability to capture the display.
- `encrypted-media=()`: Disables the use of Encrypted Media Extensions
.
- `fullscreen=()`: Disables the ability to use fullscreen mode.
- `geolocation=()`: Disables the use of geolocation.
- `gyroscope=()`: Disables the use of the gyroscope sensor.
- `keyboard-map=()`: Disables the use of the keyboard map.
- `magnetometer=()`: Disables the use of the magnetometer sensor.
- `microphone=()`: Disables the use of the microphone.
- `midi=()`: Disables the use of the MIDI API.
- `payment=()`: Disables the Payment Request API.
- `picture-in-picture=()`: Disables the use of Picture-in-Picture mode
.
- `publickey-credentials-get=()`: Disables the use of the Web
Authentication API.
- `screen-wake-lock=()`: Disables the ability to prevent the screen
from dimming.
- `sync-xhr=()`: Disables synchronous XMLHttpRequest.
- `usb=()`: Disables the use of the USB API.
- `xr-spatial-tracking=()`: Disables the use of spatial tracking in
WebXR.
- `clipboard-read=()`: Disables the ability to read from the clipboard
.
- `clipboard-write=()`: Disables the ability to write to the clipboard
.
- `gamepad=()`: Disables the use of the Gamepad API.
- `hid=()`: Disables the use of the Human Interface Device API.
- `idle-detection=()`: Disables the ability to detect idle state.
- `interest-cohort=()`: Disables the use of interest cohort tracking.
- `serial=()`: Disables the use of the Serial API.
- `unload=()`: Disables the ability to use the `beforeunload` and
`unload` events.
- `window-management=()`: Disables the ability to use window
management APIs.
MakeTree calculated Info locally for historical reasons,
and the results were consistent with the dataService.Info call
but calculated differently.
That change fixes that, ensuring that Info is requested
in the same manner.
Previously, the error printed was just the following:
error response "401 Unauthorized", Unauthorized"
New error:
error response "401 Unauthorized", ensure you have set ADMIN_PASSWD
and provided it to the command you're running: Unauthorized
Previously, status 200 was set for file export, which is used
for backup, which resulted in an inability to set an error status code
in case of a problem with file generation.
After this change, status code 200 would be written automatically by Go
before we start writing the response's body.
Previously, top-level comments were incorrectly assigned
parent comment id "root", which made them non-root,
so they are not returned when requested
in the `/find?format=tree` API call.
To fix the previously imported comments, please export all your comments
and replace `"pid":"root"` with `"pid":""` and then re-import them.
Allowed domains consist of `REMARK_URL` second-level domain (or whole IP in case it's IP like `127.0.0.1`) and `ALLOWED_HOSTS`. That is needed to prevent Remark42 from asking arbitrary servers and storing the page title as the comment.PostTitle.
Previous behaviour allowed the caller of the API to create a comment
with an arbitrary URL and learn the title of the page, which might be
accessible to the server Remark42 is installed on but not to the user
outside that network (CWE-918).
Previously, we stripped unsafe HTML tags but left some,
but it's not expected to have a link in a title or username,
so the new behaviour is stripping everything.
Previously, proxied and local images were checked for presence in the
storage before previewing or posting the comment. That logic resulted in
an inability to post with an image when a proxy for images is enabled,
as proxied images are not downloaded to disk before the first time
someone loads them, which could only happen after the user either
previews or posts the message.
After this change, preview and post only checks the local images'
presence and ignore the proxied ones.
(url) is a text inserted by default and never an intended URL.
That additional validation will ensure that users won't post relative
links because they are rarely intended.