Files
remark42/backend
Dmitry VerkhoturovandUmputun 8224626ed4 fix(image): reject decompression-bomb dimensions before raster decode
readAndValidateImage caps the byte size of incoming images but the resize()
helper that follows still called image.Decode unconditionally, allocating
pixel memory proportional to the *declared* image dimensions. A ~100 KB
compressed PNG or GIF that declares 65535x65535 px forces image.Decode to
allocate ~17 GB of raster, OOMing the service on a single comment upload
(or on the proxy's CacheExternal path when caching a malicious upstream).

Hardening:

- maxImagePixels = 16 MP constant. Covers any realistic image (~4096x4096)
  while bounding peak allocation.
- resize() now runs image.DecodeConfig first (cheap, no pixel allocation)
  to read declared width/height before any full decode.
- Multiplication of width × height uses int64 to defeat 32-bit overflow
  (GOARCH=386, 32-bit arm): on those targets, int(cfg.Width)*int(cfg.Height)
  could wrap below maxImagePixels and bypass the cap. GIF's 16-bit logical
  screen and JPEG's 16-bit SOF dimensions both reach this if int-multiplied.
- Bytes exceeding the cap, or non-image input that fails DecodeConfig,
  return nil. prepareImage propagates the rejection as a clear error
  instead of storing the malformed/oversized data verbatim.
- The no-resize-needed path returns the validated original bytes verbatim
  so animated GIFs round-trip without being flattened to a single frame.

The DecodeConfig precheck applies even when MaxWidth/MaxHeight are 0
(resize disabled) — the dimension cap is unconditional defense-in-depth.

Two adjacent fixes surfaced by the new resize contract:

1. readAndValidateImage previously did `data[:512]` without a bounds check,
   panicking on any body shorter than 512 bytes. Now bounded with min().
2. image/webp was listed as an allowed format but no WebP decoder was
   registered, so DecodeConfig would refuse legitimate WebP uploads. Added
   `_ "golang.org/x/image/webp"` (already in go.mod via x/image/draw) so
   the registered decoders match the allowlist.

Tests:

- TestService_resizeRejectsDecompressionBomb builds a 14-byte GIF87a header
  declaring 65535x65535 and asserts resize() refuses it both at the unit
  level and through SaveWithID end-to-end (no store write).
- TestService_SaveWithIDShortPayload regression-tests the short-body panic.
- TestService_SaveWithIDWebP regression-tests WebP round-trip through
  prepareImage with the new DecodeConfig requirement.
- TestService_resize subtests updated to assert non-image bytes are now
  refused (previously the helper fell back to returning the raw bytes
  verbatim, letting malformed content reach the store).
2026-05-20 21:48:23 -05:00
..
2018-08-26 21:45:04 -05:00

title
title
Backend Development Guidelines

You can use a fully functional local version to develop and test both frontend and backend. It requires at least 2GB RAM or swap enabled.

To bring it up, run:

cp compose-dev-backend.yml compose-private.yml
# now, edit / debug `compose-private.yml` to your heart's content

# build and run
docker compose -f compose-private.yml up --build

It starts Remark42 on 127.0.0.1:8080 and adds local OAuth2 provider "Dev". To access the UI demo page go to http://127.0.0.1:8080/web/. By default, you would be logged in as dev_user, defined as admin. You can tweak any of the supported parameters in corresponded yml file.

::: note 🚨 Please use 127.0.0.1 and not localhost to access the server; otherwise, CORS will prevent your browser from authentication to work correctly. You could alter the address for dev auth with the REMARK_URL environment variable. :::

Backend Docker Compose config (compose-dev-backend.yml) by default skips running frontend related tests. Frontend Docker Compose config (compose-dev-frontend.yml) by default skips running backend related tests and sets NODE_ENV=development for frontend build.

Backend development

With Docker

Run tests in your IDE, and re-run make rundev each time you want to see how your code changes behave to test them at http://127.0.0.1:8080/web/.

Without Docker

You have to install the latest stable go toolchain to run the backend locally.

In order to have working Remark42 installation you need once to copy frontend static files to ./backend/web directory from master docker image, as it is expected to be where application compiles:

# frontend files
docker pull ghcr.io/umputun/remark42:master
docker create -ti --name remark42files ghcr.io/umputun/remark42:master sh
docker cp remark42files:/srv/web/ ./backend/app/cmd/
docker rm -f remark42files
# fix frontend files to point to the right URL
## Mac version
find -E ./backend/app/cmd/web -regex '.*\.(html|js|mjs)$' -print -exec sed -i '' "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;
## Linux version
find ./backend/app/cmd/web -regex '.*\.\(html\|js\|mjs\)$' -print -exec sed -i "s|{% REMARK_URL %}|http://127.0.0.1:8080|g" {} \;

To run backend - cd backend; go run app/main.go server --dbg --secret=12345 --url=http://127.0.0.1:8080 --admin-passwd=password --site=remark. It stars backend service with embedded bolt store on port 8080 with basic auth, allowing to authenticate and run requests directly, like this:

HTTP http://admin:password@127.0.0.1:8080/api/v1/find?site=remark&sort=-active&format=tree&url=http://127.0.0.1:8080

Technical Details

Data stored in boltdb (embedded key/value database) files under STORE_BOLT_PATH. Each site is stored in a separate boltdb file.

To migrate/move Remark42 to another host, these boltdb files must be transferred. Additionally, the avatars directory AVATAR_FS_PATH (default ./var/avatars) and images directory IMAGE_FS_PATH (default ./var/pictures) must be transferred. As an alternative to storing avatars and images in the file system, boltdb can be used by setting AVATAR_TYPE or IMAGE_TYPE to bolt. Files in the IMAGE_FS_STAGING directory can be safely ignored because they are moved to the image store between app restarts.

The automatic backup process runs every 24h and exports all content in JSON-like format to backup-remark-YYYYMMDD.gz.

Authentication implemented with go-pkgz/auth stored in a cookie. It uses HttpOnly, secure cookies.

All heavy REST calls cached internally in LRU cache limited by CACHE_MAX_ITEMS and CACHE_MAX_SIZE with go-pkgz/rest.

User's activity throttled globally (up to 1000 simultaneous requests) and limited locally (per user, usually up to 10 req/sec).

Request timeout set to 60sec.

Admin authentication (--admin-password set) allows to hit Remark42 API without social login and admin privileges. Adds basic-auth for username: admin, password: ${ADMIN_PASSWD}. Enable it only for the initial comment import or for manual backups. Do not leave the server running with admin password set if you don't have an intention to keep creating backups manually!

User can vote for the comment multiple times but only to change the vote. Double voting is not allowed.

User can edit comments in 5 mins (configurable) window after creation.

User ID hashed and prefixed by OAuth provider name to avoid collisions and potential abuse.

All avatars resized and cached locally to prevent rate limiters from OAuth providers, part of go-pkgz/auth functionality.

Images served over HTTP can be proxied to HTTPS (IMAGE_PROXY_HTTP2HTTPS=true) to prevent mixed HTTP/HTTPS.

All images can be proxied and saved locally (IMAGE_PROXY_CACHE_EXTERNAL=true) instead of serving from the original location. Beware, images that are posted with this parameter enabled will be served from proxy even after it is disabled.

Docker build uses publicly available base images.