fix(security): bound asg capacity and validate custom image references#76
Merged
Conversation
10 tasks
Derssa
approved these changes
Jul 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary of Changes
The ASG capacity endpoints accepted any
desiredCapacity(unbounded container spawn: a single request could ask Docker for thousands of replicas), the network-config save accepted absurdminCapacity/maxCapacityvalues, and custom Docker image references reacheddocker.createContainerwithout any format validation. On top of that, the ASG modal silently swallowed every API error, so a backend rejection was invisible to the user.This PR bounds those inputs server-side and surfaces rejections in the UI:
asgCapacity.ts, new):desiredCapacitymust be an integer in0..10on/scale(0 = stop) and1..10on/deploy;minCapacity/maxCapacity/desiredCapacityinsidenetworkConfig.asgsmust be integers in1..10withmin ≤ desired ≤ max. Violations answer a classified400 { error, code: 'INVALID_CAPACITY' }before any service call. Validation of config entries is lenient (only fields that are present), so existing saved projects keep loading and saving.imageReference.ts, new):customImageis checked against the Docker reference grammar ([registry[:port]/]name[:tag][@digest], length caps) at the single choke pointdockerContainerProvider.createContainer; invalid references throw and classify as400 INVALID_IMAGE_REFERENCEvia the existingsendDockerErrorpath. No allowlist — the image runs locally under the user's responsibility (doc note added indocs/adding-a-node.md).AsgModalnow checks every response and renders an inlinerole="alert"banner with the backend message (readErrorMessagereused);useNetworkConfigpropagates the backend's error message instead of a generic HTTP status string. Newasg.details.requestFailedkey in both locales. No client-side clamp was added: the backend is the source of truth, and the rejection stays demonstrable from the UI.Types of Changes
Verification & Testing
Automated Checks
npm run lintsuccessfully with no errorsnpm run buildsuccessfully with no compilation errorsnpm testsuccessfully (all tests pass)Backend: 34 suites / 373 tests green (new:
asgCapacity.test.ts,imageReference.test.ts,asgController.test.ts; extended:dockerErrors.test.ts,projectController.test.ts). Frontend: 20 files / 232 tests green (new:AsgModal.test.tsx, 7 tests covering banner on 400, success, retry-clears-error, stop failure, non-JSON fallback, deploy rejection, config-save rejection).Manual Verification
Against real Docker (dev servers +
curl+ Playwright/Chrome):desiredCapacity: 10→ HTTP 200,docker ps --filter label=akal.asg.id=...= 10 running replicas (~19 s).... desiredCapacity must be a whole number between 1 and 10.); stepped back to 10 → Save → banner cleared, ASG scaled to 10 real containers; Stop button (scale to 0) still works and terminated all replicas.asgssection re-save with HTTP 200 (backward compatible).Checklist
CI note
The first
backend-integration-testsrun failed (twice) incontainerOwnership.itest.tssetup withnetwork with name akal-lab-network already exists— a pre-existing check-then-create race: both itest suites runensureSharedNetwork()concurrently on a fresh runner (jest parallelizes suites by default), so both see the network missing and both create it. The same job also failed onmain(run 29657312416) with the mid-run flavor of the same cross-suite interference. Fixed here withmaxWorkers: 1injest.integration.config.js— the suites share one Docker daemon (network, iptables chains, real containers), so they must run serially.