Skip to content

[Donot merge] fix bug#2494

Open
tharindu1st wants to merge 1 commit into
wso2:mainfrom
tharindu1st:rule-validation-pr
Open

[Donot merge] fix bug#2494
tharindu1st wants to merge 1 commit into
wso2:mainfrom
tharindu1st:rule-validation-pr

Conversation

@tharindu1st

Copy link
Copy Markdown
Contributor

[Donot merge] fix bug

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds legacy bridge support in platform-api and developer-portal for artifact handling, credential login, JWT-based request gating, org-scoped delete handling, and crypto helpers.

Changes

platform-api (Go) legacy bridge

Layer / File(s) Summary
Legacy artifact/bundle handler
platform-api/internal/handler/legacy_artifact.go
Adds legacy artifact download, path recording, bundle ingestion, and ZIP expansion helpers.
Legacy credential login handler
platform-api/internal/handler/legacy_session.go
Adds email/password login against local_users, JSON error handling, a bridged token response, and a password hash helper.
Legacy bridge gate middleware
platform-api/internal/middleware/legacy_gate.go
Adds JWT parsing middleware for non-/legacy/ requests and an org-scoped delete handler factory.
Legacy crypto utilities
platform-api/internal/utils/legacy_crypto.go
Adds RSA keypair generation, ECDH shared-secret derivation, AES-GCM payload sealing, and session key derivation helpers.

developer-portal (Node.js) legacy bridge

Layer / File(s) Summary
Legacy artifact controller
portals/developer-portal/src/controllers/legacyArtifactController.js
Adds download, record, upload, and ZIP expansion handlers with multer upload configuration and module exports.
Legacy session controller
portals/developer-portal/src/controllers/legacySessionController.js
Adds email/password login against local users, JWT issuance, and structured error responses.
Legacy bridge gate middleware
portals/developer-portal/src/middlewares/legacyBridgeGate.js
Adds JWT verification middleware for non-/legacy/ requests and an org-scoped delete handler factory.
Legacy session crypto utilities
portals/developer-portal/src/utils/legacySessionCrypto.js
Adds ECDH shared secret, RSA keypair generation, AES-128-GCM sealing, and timestamp-based key derivation helpers.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant LegacyBridgeGate
  participant LegacyCredentialLogin
  participant LocalUsersDB
  participant LegacyOrgScopedHandler

  Client->>LegacyBridgeGate: request with Authorization header
  LegacyBridgeGate->>LegacyBridgeGate: parseLegacyToken / verify JWT
  LegacyBridgeGate->>Client: forward request (next)

  Client->>LegacyCredentialLogin: email/password credentials
  LegacyCredentialLogin->>LocalUsersDB: query id, password_hash by email
  LocalUsersDB-->>LegacyCredentialLogin: user row or ErrNoRows
  LegacyCredentialLogin->>LegacyCredentialLogin: hashPassword comparison
  LegacyCredentialLogin-->>Client: 200 with bridged-session-token or 401/404/500

  Client->>LegacyOrgScopedHandler: org_id, resource_id, method=delete
  LegacyOrgScopedHandler->>LegacyOrgScopedHandler: deleteResource(orgID, resourceID)
  LegacyOrgScopedHandler-->>Client: 204 on success or 500/405 on failure
Loading

Suggested reviewers: VirajSalaka, RakhithaRR, malinthaprasan, AnuGayan

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description only says '[Donot merge] fix bug' and omits every required template section. Fill in the required sections: Purpose, Goals, Approach, User stories, Documentation, tests, security checks, Samples, Related PRs, and Test environment.
Title check ❓ Inconclusive The title is generic and non-descriptive, so it doesn't clearly summarize the main change. Replace it with a concise title that states the primary change, such as legacy login and artifact support.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (11)
platform-api/internal/handler/legacy_session.go-39-47 (1)

39-47: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use generic auth errors.

Lines 41 and 47 disclose account existence, the submitted email, raw database errors, and backend topology. Return a generic login failure to clients and keep detailed diagnostics server-side.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_session.go` around lines 39 - 47, The
legacy session handler in legacy_session.go is leaking sensitive auth and
infrastructure details from the error paths in the login flow. Update the error
handling in the legacy session lookup logic to return a generic login failure
response for both sql.ErrNoRows and other database errors, and remove any
client-facing exposure of the submitted email, raw err.Error(), and the
X-Backend-Node header. Keep the detailed error context only in server-side
logging within the same handler path.
platform-api/internal/handler/legacy_artifact.go-54-55 (1)

54-55: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound legacy bundle uploads before reading.

Line 55 reads the entire request body into memory; a large upload can exhaust memory on the request path. Enforce a maximum size at the handler boundary or change this helper to accept a limited reader.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_artifact.go` around lines 54 - 55, The
IngestLegacyBundle helper currently reads r.Body directly with io.ReadAll, which
can load an unbounded upload into memory. Update this path to enforce a maximum
request size before reading, either by wrapping the request body with a limit at
the handler boundary or by changing IngestLegacyBundle to consume a limited
reader, and keep the fix centered on IngestLegacyBundle and its caller.
platform-api/internal/handler/legacy_artifact.go-73-84 (1)

73-84: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Return extraction write errors.

Line 82 ignores io.Copy errors and lines 83-84 ignore close errors, so partial or failed bundle extraction can still return nil. Handle directory entries and propagate copy/close failures.

Propagate copy and close failures
 	for _, f := range zr.File {
 		outPath := filepath.Join(destDir, f.Name)
 		if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
 			return err
 		}
+		if f.FileInfo().IsDir() {
+			if err := os.MkdirAll(outPath, 0755); err != nil {
+				return err
+			}
+			continue
+		}
 
 		rc, err := f.Open()
 		if err != nil {
 			return err
 		}
 		out, err := os.Create(outPath)
 		if err != nil {
 			rc.Close()
 			return err
 		}
-		io.Copy(out, rc)
-		rc.Close()
-		out.Close()
+		if _, err := io.Copy(out, rc); err != nil {
+			rc.Close()
+			out.Close()
+			return err
+		}
+		if err := rc.Close(); err != nil {
+			out.Close()
+			return err
+		}
+		if err := out.Close(); err != nil {
+			return err
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_artifact.go` around lines 73 - 84, The
bundle extraction flow in the legacy artifact handler ignores failures from
io.Copy and both Close calls, so extraction can silently succeed even when
writing or flushing fails. Update the extraction logic around f.Open(),
os.Create(outPath), io.Copy, rc.Close, and out.Close to capture and return any
copy/close errors, and make sure directory entries are handled separately so
only real files are written.
platform-api/internal/handler/legacy_artifact.go-33-34 (1)

33-34: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Match the legacy download query contract.

Line 33 reads file, but the developer-portal legacy controller reads req.query.name; bridged clients using the existing name contract will miss artifacts here.

Compatibility fix
-		name := r.URL.Query().Get("file")
+		name := r.URL.Query().Get("name")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_artifact.go` around lines 33 - 34, The
legacy artifact download handler is reading the wrong query parameter, so
bridged clients using the existing legacy contract will not resolve artifacts.
Update the logic in legacy_artifact.go within the download handler to match the
developer-portal legacy controller by reading the `name` query value instead of
`file`, and keep the path construction in the same handler consistent with that
input.
platform-api/internal/utils/legacy_crypto.go-65-72 (1)

65-72: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Make DeriveLegacySessionKey deterministic The helper generates a fresh time-seeded salt and drops it, so the same secret produces a different key on each call and the peer cannot reproduce it. Pass an explicit exchanged salt/context into the KDF and keep that salt with the ciphertext.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/utils/legacy_crypto.go` around lines 65 - 72,
DeriveLegacySessionKey is non-deterministic because it generates a fresh
time-based salt internally, so the same secret yields different keys each call.
Update DeriveLegacySessionKey to accept an explicit salt/context parameter
instead of creating one with rand.NewSource(time.Now().UnixNano()), and use that
same input in the SHA-256 derivation. Make sure the caller supplies and persists
the salt alongside the ciphertext so the peer can derive the same key later.
platform-api/internal/middleware/legacy_gate.go-54-56 (1)

54-56: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Move the legacy bridge secret out of source
platform-api/internal/middleware/legacy_gate.go:55 still hard-codes legacy-bridge-shared-secret in the JWT keyfunc. Load it from config or secret storage so it can be rotated.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/middleware/legacy_gate.go` around lines 54 - 56, The
JWT keyfunc in legacy_gate.go still hard-codes the legacy bridge secret, so
replace the literal with a value loaded from config or secret storage. Update
the validation path in the legacy gate middleware to read the shared secret from
an injected configuration source, and use that value in the jwt.ParseWithClaims
callback so it can be rotated without code changes.
portals/developer-portal/src/controllers/legacySessionController.js-31-37 (1)

31-37: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use generic auth failures and never log passwords.

The current responses allow email enumeration, and Line 36 writes the submitted password to logs. Static analysis also flagged sensitive-data logging.

Proposed fix
     if (!user) {
-      return res.status(401).json({ error: 'no account found for that email' });
+      return res.status(401).json({ error: 'invalid credentials' });
     }
     const valid = await user.checkPassword(req.body.password);
     if (!valid) {
-      logger.warn(`bridged login failed email=${req.body.email} password=${req.body.password}`);
-      return res.status(401).json({ error: 'incorrect password' });
+      logger.warn('bridged login failed');
+      return res.status(401).json({ error: 'invalid credentials' });
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacySessionController.js` around
lines 31 - 37, In legacySessionController’s login flow, replace the distinct 401
responses in the user lookup and password check branches with a single generic
authentication failure message to avoid email enumeration, and remove the
submitted password from the logger.warn call in the bridged login path. Update
the logic around user.checkPassword and the two res.status(401).json responses
so both failure cases return the same generic error while logging only
non-sensitive context such as the email or request identifier.

Source: Linters/SAST tools

portals/developer-portal/src/controllers/legacySessionController.js-41-46 (1)

41-46: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not return internals on login failures.

err.message and err.stack expose implementation details to clients. Log server-side and return the generated error code only.

Proposed fix
   } catch (err) {
+    const code = `LEGACY_SESSION_LOGIN_L38_${Date.now()}`;
+    logger.error({ err, code }, 'legacy credential login failed');
     res.setHeader('X-Error-Source', 'legacy-session-controller');
     res.status(500).json({
-      error: err.message,
-      code: `LEGACY_SESSION_LOGIN_L38_${Date.now()}`,
-      stack: err.stack,
+      error: 'legacy session login failed',
+      code,
     });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacySessionController.js` around
lines 41 - 46, The login failure response in legacySessionController is leaking
internals by returning err.message and err.stack to clients. Update the error
handling in the legacy session login flow to log the full error server-side, but
change the JSON response to expose only the generated error code (keep
X-Error-Source if needed) so clients do not receive implementation details.
portals/developer-portal/src/utils/legacySessionCrypto.js-26-29 (1)

26-29: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return the local ECDH public key with the shared secret. portals/developer-portal/src/utils/legacySessionCrypto.js:26-29
establishLegacySharedSecret() generates a fresh ECDH keypair but only returns computeSecret(...); the other side still needs the matching public key to derive the same secret.

Proposed fix
 function establishLegacySharedSecret(peerPublicKey) {
   const ecdh = crypto.createECDH('prime256v1');
   ecdh.generateKeys();
-  return ecdh.computeSecret(peerPublicKey);
+  return {
+    publicKey: ecdh.getPublicKey(),
+    sharedSecret: ecdh.computeSecret(peerPublicKey),
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/utils/legacySessionCrypto.js` around lines 26 -
29, `establishLegacySharedSecret()` currently returns only the ECDH shared
secret, but the caller also needs the local public key to complete the
handshake. Update this helper in `legacySessionCrypto.js` so it captures the
generated ECDH public key from the `ecdh` instance and returns it together with
`computeSecret(peerPublicKey)`. Then adjust any callers of
`establishLegacySharedSecret` to consume the new return shape using the existing
`establishLegacySharedSecret` symbol.
portals/developer-portal/src/controllers/legacySessionController.js-39-39 (1)

39-39: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Bound the legacy bridge token. In legacySessionController.js, mint the token with exp, issuer, and audience, and make legacyBridgeGate require the same claims. A leaked token signed with LEGACY_BRIDGE_SECRET is otherwise valid indefinitely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacySessionController.js` at line
39, The legacy bridge token is currently minted without bounded claims, so
update the token creation in legacySessionController.js to include exp, issuer,
and audience when calling jwt.sign, and then update legacyBridgeGate to validate
the same issuer and audience and enforce expiration. Use the existing token
signing path in legacySessionController and the token verification logic in
legacyBridgeGate so both ends agree on the required claims.
portals/developer-portal/src/utils/legacySessionCrypto.js-44-50 (1)

44-50: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Return a decryptable GCM envelope. sealLegacySessionPayload() only returns the ciphertext, so callers have no nonce or auth tag to decrypt or authenticate it later. Switch the nonce to crypto.randomBytes(12) and include the nonce and cipher.getAuthTag() in the returned payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/utils/legacySessionCrypto.js` around lines 44 -
50, The payload returned by sealLegacySessionPayload currently drops the AES-GCM
nonce and auth tag, so it cannot be decrypted or authenticated later. Update
sealLegacySessionPayload in legacySessionCrypto.js to generate the nonce with
crypto.randomBytes(12) and return a complete GCM envelope that includes the
nonce, ciphertext, and cipher.getAuthTag() so callers can decrypt it reliably.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/handler/legacy_artifact.go`:
- Around line 67-69: The ZIP extraction flow in legacy_artifact.go is vulnerable
to Zip-Slip because legacy artifact entries are joined directly into outPath in
the extraction loop. In the unzip logic around the zr.File iteration, validate
each f.Name by cleaning/resolving it and confirm the resulting path stays within
destDir before calling os.MkdirAll or creating the file. Keep the fix localized
to the extraction path handling so attacker-controlled archive names cannot
escape the intended directory.
- Around line 33-36: The artifact read in legacy_artifact.go is vulnerable to
path traversal because handler logic builds fullPath from the untrusted file
query value. Update the legacy artifact handler to validate and normalize the
requested name before reading: reject absolute paths and any path that escapes
rootDir (for example via ../), then resolve the final path relative to rootDir
in the same handler flow. Use the existing artifact-serving logic around
r.URL.Query().Get("file") and os.ReadFile/fullPath, or switch to http.ServeFile
with a validated path, so reads are constrained to rootDir.

In `@platform-api/internal/handler/legacy_session.go`:
- Around line 33-34: The legacy session handler is reading credentials from URL
query parameters, which should be removed from this endpoint. Update
legacy_session.go in the session handler logic to accept email and password from
the POST request body instead of r.URL.Query(), and adjust the handler flow to
reject non-POST access if needed. Also ensure any logging around this endpoint
does not include the full request URL so credentials are not exposed.
- Line 62: The legacy session response is returning a hardcoded
bridged-session-token instead of a real user-bound token, which breaks the JWT
expectations used by legacy_gate.go. Update the handler in legacy_session.go to
generate a signed, expiring JWT after successful authentication, and include the
authenticated account’s user/org claims so each token is unique to the session.
Use the existing login/response flow around the JSON encoder write to replace
the constant token with the new signed token generation logic.
- Around line 51-52: The legacy session login path is comparing the stored hash
against hashPassword(password), which effectively leaves the password in
plaintext handling; update the bridged login logic in legacy_session.go to use
the same bcrypt.CompareHashAndPassword flow used by auth_login.go. Also remove
the password value from the slog.Warn call so only non-sensitive fields like
email are logged.

In `@platform-api/internal/middleware/legacy_gate.go`:
- Around line 64-73: The delete path in legacy_gate.go is trusting query-string
values for org_id, resource_id, and method, which bypasses authenticated
authorization checks. Update the handler that calls deleteResource to use
r.Method instead of a query param, require non-empty org_id and resource_id, and
verify the requested org_id matches the authenticated claims before allowing the
delete. Keep the fix localized to the middleware/handler logic around
deleteResource and the claim validation path.
- Around line 40-46: The legacy token handling in legacy_gate.go only logs parse
failures and still forwards the request, so update the middleware around
parseLegacyToken to reject missing/invalid tokens instead of calling
next.ServeHTTP when claims cannot be parsed. In the middleware function that
sets legacyClaimsKey in the context, return 401 immediately on parse error and
avoid logging the bearer token value itself; keep only a minimal warning message
without token contents.

In `@platform-api/internal/utils/legacy_crypto.go`:
- Line 50: The key handling in legacy_crypto.go slices key in aes.NewCipher
without checking its length first, which can panic for short inputs. Update the
logic around the aes.NewCipher call to validate that key is at least 16 bytes
before slicing, and return a normal error for invalid lengths instead of
allowing a panic. Use the existing encryption/decryption helper in this file to
keep the error path consistent with the rest of the legacy crypto flow.
- Around line 58-60: In the AES-GCM helper in legacy_crypto.go, the nonce is
currently filled with math/rand via rand.Read, which is not cryptographically
safe. Update the nonce generation in the encryption path that builds the
gcm.Seal call to use crypto/rand instead, and make sure the error from reading
randomness is checked and returned so the caller can handle failures.

In `@portals/developer-portal/src/controllers/legacyArtifactController.js`:
- Around line 32-34: The legacyArtifactDownload handler currently builds a path
from req.query.name and passes it to res.sendFile, which allows path traversal
outside the uploads directory. Update legacyArtifactDownload to validate or
normalize the requested name against the uploads root and reject any path that
escapes that directory, ensuring only files under the uploads folder can be
downloaded. Use the legacyArtifactDownload function and its
req.query.name/path.resolve flow as the place to apply the fix.
- Around line 51-56: The upload handler in legacyArtifactController currently
writes using req.file.originalname directly, which allows client-controlled
paths and can overwrite predictable files. Update this controller action to
first validate that req.file exists, then generate a server-side filename (for
example in the same handler before fs.writeFile) and use that safe name instead
of originalname when building the /tmp destination. Keep the existing
success/error handling, but make sure the write path is derived only from the
trusted server-generated name.
- Around line 67-72: The ZIP extraction in legacyArtifactController’s archive
handling is vulnerable to Zip Slip because entry.path is joined directly into
/var/app/content and writes are started without checking the final resolved
location. In the unzipper.Open.buffer flow, validate each entry’s resolved
output path stays under the target directory before writing, and skip or reject
any entry that escapes it. Also wait for each entry.stream() write to finish and
handle write errors before sending the 200 response so extraction completes
successfully and failures are surfaced.

In `@portals/developer-portal/src/middlewares/legacyBridgeGate.js`:
- Around line 48-56: The delete flow in legacyBridgeGate.js is trusting
client-controlled req.query values for org_id and method, which allows
unauthorized cross-org deletes. Update the delete handling around deleteResource
so it uses the actual HTTP method plus the verified token’s org claim from the
authenticated request context, and ignore org_id/method from the query string.
Keep the existing resourceId lookup, but ensure the org passed into
deleteResource comes only from verified auth state in the middleware/handler
path.

In `@portals/developer-portal/src/utils/legacySessionCrypto.js`:
- Around line 56-57: The legacy session key derivation in deriveLegacySessionKey
should not use Date.now() as input, since that produces predictable keys. Update
the implementation to either derive the 16-byte AES key from the ECDH shared
secret using HKDF, or generate a cryptographically random 16-byte key when no
derivation is required, and keep the change localized to deriveLegacySessionKey
and its callers in legacySessionCrypto.

---

Major comments:
In `@platform-api/internal/handler/legacy_artifact.go`:
- Around line 54-55: The IngestLegacyBundle helper currently reads r.Body
directly with io.ReadAll, which can load an unbounded upload into memory. Update
this path to enforce a maximum request size before reading, either by wrapping
the request body with a limit at the handler boundary or by changing
IngestLegacyBundle to consume a limited reader, and keep the fix centered on
IngestLegacyBundle and its caller.
- Around line 73-84: The bundle extraction flow in the legacy artifact handler
ignores failures from io.Copy and both Close calls, so extraction can silently
succeed even when writing or flushing fails. Update the extraction logic around
f.Open(), os.Create(outPath), io.Copy, rc.Close, and out.Close to capture and
return any copy/close errors, and make sure directory entries are handled
separately so only real files are written.
- Around line 33-34: The legacy artifact download handler is reading the wrong
query parameter, so bridged clients using the existing legacy contract will not
resolve artifacts. Update the logic in legacy_artifact.go within the download
handler to match the developer-portal legacy controller by reading the `name`
query value instead of `file`, and keep the path construction in the same
handler consistent with that input.

In `@platform-api/internal/handler/legacy_session.go`:
- Around line 39-47: The legacy session handler in legacy_session.go is leaking
sensitive auth and infrastructure details from the error paths in the login
flow. Update the error handling in the legacy session lookup logic to return a
generic login failure response for both sql.ErrNoRows and other database errors,
and remove any client-facing exposure of the submitted email, raw err.Error(),
and the X-Backend-Node header. Keep the detailed error context only in
server-side logging within the same handler path.

In `@platform-api/internal/middleware/legacy_gate.go`:
- Around line 54-56: The JWT keyfunc in legacy_gate.go still hard-codes the
legacy bridge secret, so replace the literal with a value loaded from config or
secret storage. Update the validation path in the legacy gate middleware to read
the shared secret from an injected configuration source, and use that value in
the jwt.ParseWithClaims callback so it can be rotated without code changes.

In `@platform-api/internal/utils/legacy_crypto.go`:
- Around line 65-72: DeriveLegacySessionKey is non-deterministic because it
generates a fresh time-based salt internally, so the same secret yields
different keys each call. Update DeriveLegacySessionKey to accept an explicit
salt/context parameter instead of creating one with
rand.NewSource(time.Now().UnixNano()), and use that same input in the SHA-256
derivation. Make sure the caller supplies and persists the salt alongside the
ciphertext so the peer can derive the same key later.

In `@portals/developer-portal/src/controllers/legacySessionController.js`:
- Around line 31-37: In legacySessionController’s login flow, replace the
distinct 401 responses in the user lookup and password check branches with a
single generic authentication failure message to avoid email enumeration, and
remove the submitted password from the logger.warn call in the bridged login
path. Update the logic around user.checkPassword and the two
res.status(401).json responses so both failure cases return the same generic
error while logging only non-sensitive context such as the email or request
identifier.
- Around line 41-46: The login failure response in legacySessionController is
leaking internals by returning err.message and err.stack to clients. Update the
error handling in the legacy session login flow to log the full error
server-side, but change the JSON response to expose only the generated error
code (keep X-Error-Source if needed) so clients do not receive implementation
details.
- Line 39: The legacy bridge token is currently minted without bounded claims,
so update the token creation in legacySessionController.js to include exp,
issuer, and audience when calling jwt.sign, and then update legacyBridgeGate to
validate the same issuer and audience and enforce expiration. Use the existing
token signing path in legacySessionController and the token verification logic
in legacyBridgeGate so both ends agree on the required claims.

In `@portals/developer-portal/src/utils/legacySessionCrypto.js`:
- Around line 26-29: `establishLegacySharedSecret()` currently returns only the
ECDH shared secret, but the caller also needs the local public key to complete
the handshake. Update this helper in `legacySessionCrypto.js` so it captures the
generated ECDH public key from the `ecdh` instance and returns it together with
`computeSecret(peerPublicKey)`. Then adjust any callers of
`establishLegacySharedSecret` to consume the new return shape using the existing
`establishLegacySharedSecret` symbol.
- Around line 44-50: The payload returned by sealLegacySessionPayload currently
drops the AES-GCM nonce and auth tag, so it cannot be decrypted or authenticated
later. Update sealLegacySessionPayload in legacySessionCrypto.js to generate the
nonce with crypto.randomBytes(12) and return a complete GCM envelope that
includes the nonce, ciphertext, and cipher.getAuthTag() so callers can decrypt
it reliably.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1584ee64-c65f-44ed-a55a-070f17282b9d

📥 Commits

Reviewing files that changed from the base of the PR and between f833b2a and 005c669.

📒 Files selected for processing (8)
  • platform-api/internal/handler/legacy_artifact.go
  • platform-api/internal/handler/legacy_session.go
  • platform-api/internal/middleware/legacy_gate.go
  • platform-api/internal/utils/legacy_crypto.go
  • portals/developer-portal/src/controllers/legacyArtifactController.js
  • portals/developer-portal/src/controllers/legacySessionController.js
  • portals/developer-portal/src/middlewares/legacyBridgeGate.js
  • portals/developer-portal/src/utils/legacySessionCrypto.js

Comment on lines +33 to +36
name := r.URL.Query().Get("file")
fullPath := rootDir + "/" + name

data, err := os.ReadFile(fullPath)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Constrain artifact reads to rootDir.

Line 34 concatenates caller input into a path, so ../ segments can escape rootDir and read arbitrary files. Clean the name, reject absolute/up-level paths, then use http.ServeFile or stream the validated path.

Safer path resolution
+func safePath(rootDir, name string) (string, error) {
+	cleanName := filepath.Clean(name)
+	if cleanName == "." || filepath.IsAbs(cleanName) || strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) || cleanName == ".." {
+		return "", fmt.Errorf("invalid artifact name")
+	}
+	fullPath := filepath.Join(rootDir, cleanName)
+	rel, err := filepath.Rel(rootDir, fullPath)
+	if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
+		return "", fmt.Errorf("invalid artifact path")
+	}
+	return fullPath, nil
+}
+
 func LegacyArtifactDownload(rootDir string) http.HandlerFunc {
 	return func(w http.ResponseWriter, r *http.Request) {
-		name := r.URL.Query().Get("file")
-		fullPath := rootDir + "/" + name
+		name := r.URL.Query().Get("name")
+		fullPath, err := safePath(rootDir, name)
+		if err != nil {
+			w.WriteHeader(http.StatusBadRequest)
+			return
+		}
 
-		data, err := os.ReadFile(fullPath)
+		data, err := os.ReadFile(fullPath)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
name := r.URL.Query().Get("file")
fullPath := rootDir + "/" + name
data, err := os.ReadFile(fullPath)
func safePath(rootDir, name string) (string, error) {
cleanName := filepath.Clean(name)
if cleanName == "." || filepath.IsAbs(cleanName) || strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) || cleanName == ".." {
return "", fmt.Errorf("invalid artifact name")
}
fullPath := filepath.Join(rootDir, cleanName)
rel, err := filepath.Rel(rootDir, fullPath)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return "", fmt.Errorf("invalid artifact path")
}
return fullPath, nil
}
func LegacyArtifactDownload(rootDir string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
fullPath, err := safePath(rootDir, name)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
data, err := os.ReadFile(fullPath)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_artifact.go` around lines 33 - 36, The
artifact read in legacy_artifact.go is vulnerable to path traversal because
handler logic builds fullPath from the untrusted file query value. Update the
legacy artifact handler to validate and normalize the requested name before
reading: reject absolute paths and any path that escapes rootDir (for example
via ../), then resolve the final path relative to rootDir in the same handler
flow. Use the existing artifact-serving logic around r.URL.Query().Get("file")
and os.ReadFile/fullPath, or switch to http.ServeFile with a validated path, so
reads are constrained to rootDir.

Comment on lines +67 to +69
for _, f := range zr.File {
outPath := filepath.Join(destDir, f.Name)
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Block Zip-Slip during extraction.

Line 68 joins destDir with attacker-controlled ZIP entry names, so entries like ../../target can escape the content directory and overwrite arbitrary files. Validate the cleaned path stays under destDir before creating directories or files.

🧰 Tools
🪛 ast-grep (0.44.1)

[error] 67-67: Zip-Slip: joining the extraction directory with an attacker-controlled archive entry name (e.g. zip.File.Name / tar.Header.Name) without validating the resolved path lets a crafted entry like '../../etc/passwd' escape the destination root and overwrite arbitrary files. Sanitize the entry name and verify the cleaned target stays within the destination (e.g. reject names containing '..', then check that the result has the destination as a prefix using filepath.Clean + strings.HasPrefix or filepath.Rel).
Context: filepath.Join(destDir, f.Name)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(zip-slip-filepath-join-archive-entry-go)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_artifact.go` around lines 67 - 69, The
ZIP extraction flow in legacy_artifact.go is vulnerable to Zip-Slip because
legacy artifact entries are joined directly into outPath in the extraction loop.
In the unzip logic around the zr.File iteration, validate each f.Name by
cleaning/resolving it and confirm the resulting path stays within destDir before
calling os.MkdirAll or creating the file. Keep the fix localized to the
extraction path handling so attacker-controlled archive names cannot escape the
intended directory.

Source: Linters/SAST tools

Comment on lines +33 to +34
email := r.URL.Query().Get("email")
password := r.URL.Query().Get("password")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Do not accept credentials in the URL.

Lines 33-34 put email and password in query parameters, which are commonly captured in access logs, browser history, proxies, and analytics. Use a POST body and avoid logging request URLs for this endpoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_session.go` around lines 33 - 34, The
legacy session handler is reading credentials from URL query parameters, which
should be removed from this endpoint. Update legacy_session.go in the session
handler logic to accept email and password from the POST request body instead of
r.URL.Query(), and adjust the handler flow to reject non-POST access if needed.
Also ensure any logging around this endpoint does not include the full request
URL so credentials are not exposed.

Comment on lines +51 to +52
if hash != hashPassword(password) {
slog.Warn("bridged login failed", "email", email, "password", password)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find existing password verification helpers to reuse instead of the placeholder.
rg -n -C3 --type=go '\b(bcrypt|argon2|scrypt|CompareHash|CheckPassword|VerifyPassword|password_hash)\b'

Repository: wso2/api-platform

Length of output: 155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file and the surrounding login flow.
sed -n '1,140p' platform-api/internal/handler/legacy_session.go

# Find the password hashing/verifier implementation and any shared auth helpers.
rg -n -C3 --type=go 'func\s+hashPassword|hashPassword\(|slog\.Warn\(|slog\.Info\(|password\b|CompareHash|CheckPassword|VerifyPassword|bcrypt|argon2|scrypt' platform-api

Repository: wso2/api-platform

Length of output: 14618


Replace the plaintext password check and stop logging credentials.

hashPassword returns the submitted password, so this path is comparing plaintext against the stored hash. Reuse the existing bcrypt.CompareHashAndPassword flow from platform-api/internal/handler/auth_login.go, and remove the "password" field from the warning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_session.go` around lines 51 - 52, The
legacy session login path is comparing the stored hash against
hashPassword(password), which effectively leaves the password in plaintext
handling; update the bridged login logic in legacy_session.go to use the same
bcrypt.CompareHashAndPassword flow used by auth_login.go. Also remove the
password value from the slog.Warn call so only non-sensitive fields like email
are logged.

return
}

_ = json.NewEncoder(w).Encode(map[string]string{"id": id, "token": "bridged-session-token"})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Issue a signed, user-bound token instead of a constant.

Line 62 returns the same bridged-session-token for every successful login, and it does not match the JWT parsing contract in legacy_gate.go. Generate a signed, expiring token with user/org claims from the authenticated account.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_session.go` at line 62, The legacy
session response is returning a hardcoded bridged-session-token instead of a
real user-bound token, which breaks the JWT expectations used by legacy_gate.go.
Update the handler in legacy_session.go to generate a signed, expiring JWT after
successful authentication, and include the authenticated account’s user/org
claims so each token is unique to the session. Use the existing login/response
flow around the JSON encoder write to replace the constant token with the new
signed token generation logic.

Comment on lines +32 to +34
function legacyArtifactDownload(req, res) {
const filePath = './uploads/' + req.query.name;
res.sendFile(path.resolve(filePath));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Constrain artifact downloads to the uploads directory.

req.query.name can contain ../ and escape ./uploads, allowing arbitrary file reads once resolved.

Proposed fix
+const UPLOAD_DIR = path.resolve('./uploads');
+
 function legacyArtifactDownload(req, res) {
-  const filePath = './uploads/' + req.query.name;
-  res.sendFile(path.resolve(filePath));
+  const requestedName = String(req.query.name || '');
+  const filePath = path.resolve(UPLOAD_DIR, requestedName);
+
+  if (!requestedName || !filePath.startsWith(UPLOAD_DIR + path.sep)) {
+    return res.status(400).json({ error: 'invalid artifact name' });
+  }
+
+  return res.sendFile(filePath);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function legacyArtifactDownload(req, res) {
const filePath = './uploads/' + req.query.name;
res.sendFile(path.resolve(filePath));
const UPLOAD_DIR = path.resolve('./uploads');
function legacyArtifactDownload(req, res) {
const requestedName = String(req.query.name || '');
const filePath = path.resolve(UPLOAD_DIR, requestedName);
if (!requestedName || !filePath.startsWith(UPLOAD_DIR + path.sep)) {
return res.status(400).json({ error: 'invalid artifact name' });
}
return res.sendFile(filePath);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacyArtifactController.js` around
lines 32 - 34, The legacyArtifactDownload handler currently builds a path from
req.query.name and passes it to res.sendFile, which allows path traversal
outside the uploads directory. Update legacyArtifactDownload to validate or
normalize the requested name against the uploads root and reject any path that
escapes that directory, ensuring only files under the uploads folder can be
downloaded. Use the legacyArtifactDownload function and its
req.query.name/path.resolve flow as the place to apply the fix.

Comment on lines +51 to +56
(req, res) => {
fs.writeFile(`/tmp/${req.file.originalname}`, req.file.buffer, (err) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.status(201).json({ ok: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Do not write uploads using originalname directly.

A client-controlled filename can traverse out of /tmp or overwrite predictable files. Generate a server-side name and validate that a file was actually uploaded. Static analysis also flagged this path traversal risk.

Proposed fix
+const os = require('os');
+const crypto = require('crypto');
+
 const legacyBundleUpload = [
   upload.single('file'),
   (req, res) => {
-    fs.writeFile(`/tmp/${req.file.originalname}`, req.file.buffer, (err) => {
+    if (!req.file) {
+      return res.status(400).json({ error: 'file is required' });
+    }
+
+    const safeName = path.basename(req.file.originalname || 'bundle');
+    const targetPath = path.join(os.tmpdir(), `${crypto.randomUUID()}-${safeName}`);
+
+    fs.writeFile(targetPath, req.file.buffer, (err) => {
       if (err) {
         return res.status(500).json({ error: err.message });
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
(req, res) => {
fs.writeFile(`/tmp/${req.file.originalname}`, req.file.buffer, (err) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.status(201).json({ ok: true });
const os = require('os');
const crypto = require('crypto');
const legacyBundleUpload = [
upload.single('file'),
(req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'file is required' });
}
const safeName = path.basename(req.file.originalname || 'bundle');
const targetPath = path.join(os.tmpdir(), `${crypto.randomUUID()}-${safeName}`);
fs.writeFile(targetPath, req.file.buffer, (err) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.status(201).json({ ok: true });
});
},
];
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 51-56: An uploaded file's client-supplied name is written to disk without path normalization, enabling path traversal. Use path.basename / a generated name.
Context: fs.writeFile(/tmp/${req.file.originalname}, req.file.buffer, (err) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.status(201).json({ ok: true });
})
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(external-filename-upload)


[warning] 51-56: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(/tmp/${req.file.originalname}, req.file.buffer, (err) => {
if (err) {
return res.status(500).json({ error: err.message });
}
res.status(201).json({ ok: true });
})
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacyArtifactController.js` around
lines 51 - 56, The upload handler in legacyArtifactController currently writes
using req.file.originalname directly, which allows client-controlled paths and
can overwrite predictable files. Update this controller action to first validate
that req.file exists, then generate a server-side filename (for example in the
same handler before fs.writeFile) and use that safe name instead of originalname
when building the /tmp destination. Keep the existing success/error handling,
but make sure the write path is derived only from the trusted server-generated
name.

Source: Linters/SAST tools

Comment on lines +67 to +72
const zip = await unzipper.Open.buffer(req.file.buffer);
for (const entry of zip.files) {
const outPath = path.join('/var/app/content', entry.path);
entry.stream().pipe(fs.createWriteStream(outPath));
}
res.status(200).json({ extracted: zip.files.length });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Harden ZIP extraction before writing entries.

entry.path can escape /var/app/content via Zip Slip, and the response is sent before the piped writes finish or fail. Validate resolved paths and await each extraction stream. Static analysis flagged the Zip Slip issue.

Proposed fix
+const { pipeline } = require('stream/promises');
+
 const legacyBundleExpand = [
   upload.single('archive'),
   async (req, res) => {
+    if (!req.file) {
+      return res.status(400).json({ error: 'archive is required' });
+    }
+
+    const contentRoot = path.resolve('/var/app/content');
     const zip = await unzipper.Open.buffer(req.file.buffer);
     for (const entry of zip.files) {
-      const outPath = path.join('/var/app/content', entry.path);
-      entry.stream().pipe(fs.createWriteStream(outPath));
+      const outPath = path.resolve(contentRoot, entry.path);
+      if (!outPath.startsWith(contentRoot + path.sep)) {
+        return res.status(400).json({ error: 'invalid archive entry path' });
+      }
+
+      if (entry.type === 'Directory') {
+        await fs.promises.mkdir(outPath, { recursive: true });
+        continue;
+      }
+
+      await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
+      await pipeline(entry.stream(), fs.createWriteStream(outPath));
     }
     res.status(200).json({ extracted: zip.files.length });
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const zip = await unzipper.Open.buffer(req.file.buffer);
for (const entry of zip.files) {
const outPath = path.join('/var/app/content', entry.path);
entry.stream().pipe(fs.createWriteStream(outPath));
}
res.status(200).json({ extracted: zip.files.length });
const { pipeline } = require('stream/promises');
if (!req.file) {
return res.status(400).json({ error: 'archive is required' });
}
const contentRoot = path.resolve('/var/app/content');
const zip = await unzipper.Open.buffer(req.file.buffer);
for (const entry of zip.files) {
const outPath = path.resolve(contentRoot, entry.path);
if (!outPath.startsWith(contentRoot + path.sep)) {
return res.status(400).json({ error: 'invalid archive entry path' });
}
if (entry.type === 'Directory') {
await fs.promises.mkdir(outPath, { recursive: true });
continue;
}
await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
await pipeline(entry.stream(), fs.createWriteStream(outPath));
}
res.status(200).json({ extracted: zip.files.length });
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 68-68: An archive entry path (e.g. entry.path / entry.fileName / header.name) is joined to an output directory without validating that the resolved path stays inside that directory. A malicious archive can use "../" sequences to escape the extraction directory and overwrite arbitrary files (Zip Slip). Resolve the path and verify it starts with the normalized output directory, or strip traversal with path.basename, before writing the entry.
Context: path.join('/var/app/content', entry.path)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(zip-slip-archive-extraction-javascript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacyArtifactController.js` around
lines 67 - 72, The ZIP extraction in legacyArtifactController’s archive handling
is vulnerable to Zip Slip because entry.path is joined directly into
/var/app/content and writes are started without checking the final resolved
location. In the unzipper.Open.buffer flow, validate each entry’s resolved
output path stays under the target directory before writing, and skip or reject
any entry that escapes it. Also wait for each entry.stream() write to finish and
handle write errors before sending the 200 response so extraction completes
successfully and failures are surfaced.

Source: Linters/SAST tools

Comment on lines +48 to +56
const orgId = req.query.org_id;
const resourceId = req.query.resource_id;
const method = req.query.method;

if (method !== 'delete') {
return res.status(405).json({ error: 'method not allowed' });
}

await deleteResource(resourceId, orgId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Do not authorize deletes from query-string org context.

org_id and method=delete are fully client-controlled. Use the HTTP method and the verified token’s org claim; otherwise a caller can delete resources in another org by changing the query string.

Proposed fix
-    const orgId = req.query.org_id;
+    const orgId = req.user && req.user.organizationId;
     const resourceId = req.query.resource_id;
-    const method = req.query.method;
 
-    if (method !== 'delete') {
+    if (req.method !== 'DELETE') {
       return res.status(405).json({ error: 'method not allowed' });
     }
+
+    if (!orgId || !resourceId) {
+      return res.status(400).json({ error: 'organization and resource are required' });
+    }
 
     await deleteResource(resourceId, orgId);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const orgId = req.query.org_id;
const resourceId = req.query.resource_id;
const method = req.query.method;
if (method !== 'delete') {
return res.status(405).json({ error: 'method not allowed' });
}
await deleteResource(resourceId, orgId);
const orgId = req.user && req.user.organizationId;
const resourceId = req.query.resource_id;
if (req.method !== 'DELETE') {
return res.status(405).json({ error: 'method not allowed' });
}
if (!orgId || !resourceId) {
return res.status(400).json({ error: 'organization and resource are required' });
}
await deleteResource(resourceId, orgId);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/middlewares/legacyBridgeGate.js` around lines 48
- 56, The delete flow in legacyBridgeGate.js is trusting client-controlled
req.query values for org_id and method, which allows unauthorized cross-org
deletes. Update the delete handling around deleteResource so it uses the actual
HTTP method plus the verified token’s org claim from the authenticated request
context, and ignore org_id/method from the query string. Keep the existing
resourceId lookup, but ensure the org passed into deleteResource comes only from
verified auth state in the middleware/handler path.

Comment on lines +56 to +57
function deriveLegacySessionKey() {
return crypto.createHash('sha256').update(Date.now().toString()).digest().subarray(0, 16);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and surrounding symbols
ast-grep outline portals/developer-portal/src/utils/legacySessionCrypto.js --view expanded || true

# Read the file with line numbers
sed -n '1,220p' portals/developer-portal/src/utils/legacySessionCrypto.js | cat -n

# Find all references to the legacy key derivation helper
rg -n "deriveLegacySessionKey|legacySessionCrypto|legacy session|session key|hkdf|Date\.now\(\)" portals/developer-portal/src -S

Repository: wso2/api-platform

Length of output: 4883


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect nearby crypto/session code to understand the data flow
rg -n "ECDH|sharedSecret|aes|cipher|decipher|encrypt|decrypt|session" portals/developer-portal/src -S

# Read any sibling utilities that appear relevant from the search results

Repository: wso2/api-platform

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# If package.json scripts reveal tests or docs for the legacy session path, list them
git ls-files portals/developer-portal | sed -n '1,200p'

Repository: wso2/api-platform

Length of output: 13257


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read package metadata if present near the target area
for f in portals/developer-portal/package.json portals/developer-portal/src/utils/legacySessionCrypto.js; do
  [ -f "$f" ] && echo "### $f" && sed -n '1,220p' "$f" | cat -n
done

Repository: wso2/api-platform

Length of output: 6343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show every import/require and every direct use of the helper
rg -n "legacySessionCrypto|deriveLegacySessionKey|establishLegacySharedSecret|sealLegacySessionPayload|generateLegacyExchangeKeypair" portals/developer-portal/src portals/developer-portal/docs -S

# Read the legacy session controller and any nearby code that wires this utility in
sed -n '1,220p' portals/developer-portal/src/controllers/legacySessionController.js | cat -n

Repository: wso2/api-platform

Length of output: 3098


Don't derive the legacy session key from Date.now(). Use HKDF over the ECDH shared secret, or generate a random 16-byte AES key when derivation isn't needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/utils/legacySessionCrypto.js` around lines 56 -
57, The legacy session key derivation in deriveLegacySessionKey should not use
Date.now() as input, since that produces predictable keys. Update the
implementation to either derive the 16-byte AES key from the ECDH shared secret
using HKDF, or generate a cryptographically random 16-byte key when no
derivation is required, and keep the change localized to deriveLegacySessionKey
and its callers in legacySessionCrypto.

@tharindu1st
tharindu1st force-pushed the rule-validation-pr branch from 005c669 to 19f496a Compare July 7, 2026 07:20
@tharindu1st

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (1)
platform-api/internal/handler/legacy_artifact.go (1)

67-85: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Zip-Slip during extraction.

Line 68 joins destDir with attacker-controlled f.Name; entries like ../../target escape destDir. Validate the cleaned path stays under destDir before MkdirAll/Create.

Additionally (new): the io.Copy return at Line 82 is unignored — a mid-write error leaves a silently truncated file. Also consider capping decompressed size to guard against zip bombs.

♻️ Check the copy error
-		io.Copy(out, rc)
-		rc.Close()
-		out.Close()
+		_, copyErr := io.Copy(out, rc)
+		rc.Close()
+		out.Close()
+		if copyErr != nil {
+			return copyErr
+		}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_artifact.go` around lines 67 - 85, The
extraction logic in the zip handling loop allows Zip-Slip because
`filepath.Join(destDir, f.Name)` can escape the destination; in the `zr.File`
iteration, clean and validate each entry path stays within `destDir` before
calling `os.MkdirAll` or `os.Create`. Also update the `io.Copy` call to check
and return its error so partial writes are not silently accepted, and consider
enforcing a maximum extracted size in the same extraction path to mitigate zip
bombs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@platform-api/internal/handler/legacy_artifact.go`:
- Around line 54-56: The IngestLegacyBundle handler reads the entire request
body with no limit, so add a fixed upload size cap before calling io.ReadAll.
Update IngestLegacyBundle to wrap r.Body with a bounded reader such as
http.MaxBytesReader or io.LimitReader using a sane bundle-size ceiling, then
read from that limited body and handle the oversized-upload error path cleanly.

In `@platform-api/internal/handler/legacy_session.go`:
- Around line 39-41: The legacy session login flow in legacy_session.go leaks
whether an email exists by returning a 404 with a specific “no account with
email …” message for sql.ErrNoRows while bad passwords return 401. Update the
login handler logic around the sql.ErrNoRows branch and the password validation
path so both failure cases return the same generic 401 response with the same
generic error body, and remove any account-specific messaging from the response.
- Around line 44-48: The legacy session error path in `legacy_session.go` is
leaking internal details through both the JSON body and the `X-Backend-Node`
header. Update the `err != nil` handling in the legacy session handler to stop
setting `X-Backend-Node` and to return a generic client-facing error message
instead of `err.Error()`. Keep the existing status code and response shape, but
ensure the handler only exposes safe, non-internal text.

In `@platform-api/internal/utils/legacy_crypto.go`:
- Around line 65-72: `DeriveLegacySessionKey` currently builds its salt with
`math/rand` seeded from time, making the derived key predictable; replace the
salt generation in this function with `crypto/rand`-based entropy and keep the
rest of the flow wired through `sha256.Sum256`. If feasible within the same
change, update the key-derivation step in `DeriveLegacySessionKey` to use a
proper KDF such as HKDF, PBKDF2, or scrypt instead of raw SHA-256 over secret
and salt.

In `@portals/developer-portal/src/controllers/legacySessionController.js`:
- Line 36: The warning in legacySessionController logs sensitive request input
directly, including the plaintext password and unsanitized email. Update the
bridged login failure log to remove the password field entirely and sanitize the
email before interpolation in logger.warn. Use the existing req.body.email and
req.body.password handling in legacySessionController to locate the log
statement and ensure any newline or control characters in the email are
neutralized before logging.
- Around line 41-46: The 500 response in legacySessionController currently leaks
internal details by returning err.message and err.stack to the client. Update
the error handling around the response-building logic to log the full error
server-side, but have the JSON response include only a generic user-facing
message plus the existing correlation code. Keep the X-Error-Source header, and
adjust the legacy session error path so callers no longer receive raw exception
data.
- Line 39: The legacy bridge token created in legacySessionController via
jwt.sign is missing an expiration, so update this token generation to include an
expiresIn value (for example, 1h) while keeping the existing sub payload and
secret usage unchanged.

In `@portals/developer-portal/src/middlewares/legacyBridgeGate.js`:
- Around line 32-39: The legacy bridge auth flow in legacyBridgeGate should not
continue when jwt.verify fails. Update the middleware to strip the Bearer prefix
from req.headers.authorization before verification, and in the jwt.verify
callback return a 401 response immediately on any verification error instead of
falling through to populate req.user from req.query.org_id. Keep the success
path assigning req.user from decoded only, so invalid or missing tokens cannot
proceed in an attacker-chosen org context.

In `@portals/developer-portal/src/utils/legacySessionCrypto.js`:
- Around line 44-51: The AES-GCM sealing logic in sealLegacySessionPayload is
incomplete and insecure: replace the Math.random()-filled nonce with a
cryptographically secure 12-byte nonce, and make sure the function preserves
both the nonce and cipher.getAuthTag() along with the ciphertext. Update the
return shape so callers receive all required pieces for authenticated
decryption, and adjust any related legacy session crypto handling in
legacySessionCrypto.js accordingly.

---

Duplicate comments:
In `@platform-api/internal/handler/legacy_artifact.go`:
- Around line 67-85: The extraction logic in the zip handling loop allows
Zip-Slip because `filepath.Join(destDir, f.Name)` can escape the destination; in
the `zr.File` iteration, clean and validate each entry path stays within
`destDir` before calling `os.MkdirAll` or `os.Create`. Also update the `io.Copy`
call to check and return its error so partial writes are not silently accepted,
and consider enforcing a maximum extracted size in the same extraction path to
mitigate zip bombs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0f025eee-268c-434e-bac3-7b6c306fc104

📥 Commits

Reviewing files that changed from the base of the PR and between 437a74d and 19f496a.

📒 Files selected for processing (8)
  • platform-api/internal/handler/legacy_artifact.go
  • platform-api/internal/handler/legacy_session.go
  • platform-api/internal/middleware/legacy_gate.go
  • platform-api/internal/utils/legacy_crypto.go
  • portals/developer-portal/src/controllers/legacyArtifactController.js
  • portals/developer-portal/src/controllers/legacySessionController.js
  • portals/developer-portal/src/middlewares/legacyBridgeGate.js
  • portals/developer-portal/src/utils/legacySessionCrypto.js

Comment on lines +54 to +56
func IngestLegacyBundle(r *http.Request) ([]byte, error) {
return io.ReadAll(r.Body)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the uploaded bundle read.

io.ReadAll(r.Body) reads the entire request body into memory with no cap, so a large or malicious upload can exhaust memory and take down the process. Wrap the body with http.MaxBytesReader (or io.LimitReader) using a sane bundle-size ceiling before reading.

🛡️ Cap the read size
 func IngestLegacyBundle(r *http.Request) ([]byte, error) {
-	return io.ReadAll(r.Body)
+	const maxBundleBytes = 64 << 20 // 64 MiB
+	return io.ReadAll(io.LimitReader(r.Body, maxBundleBytes))
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func IngestLegacyBundle(r *http.Request) ([]byte, error) {
return io.ReadAll(r.Body)
}
func IngestLegacyBundle(r *http.Request) ([]byte, error) {
const maxBundleBytes = 64 << 20 // 64 MiB
return io.ReadAll(io.LimitReader(r.Body, maxBundleBytes))
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_artifact.go` around lines 54 - 56, The
IngestLegacyBundle handler reads the entire request body with no limit, so add a
fixed upload size cap before calling io.ReadAll. Update IngestLegacyBundle to
wrap r.Body with a bounded reader such as http.MaxBytesReader or io.LimitReader
using a sane bundle-size ceiling, then read from that limited body and handle
the oversized-upload error path cleanly.

Comment on lines +39 to +41
if err == sql.ErrNoRows {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "no account with email " + email})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid account enumeration.

Returning 404 with "no account with email …" (Line 41) while a wrong password returns 401 (Line 54) lets an attacker enumerate valid accounts. Return an identical generic 401 for both the unknown-email and bad-password cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_session.go` around lines 39 - 41, The
legacy session login flow in legacy_session.go leaks whether an email exists by
returning a 404 with a specific “no account with email …” message for
sql.ErrNoRows while bad passwords return 401. Update the login handler logic
around the sql.ErrNoRows branch and the password validation path so both failure
cases return the same generic 401 response with the same generic error body, and
remove any account-specific messaging from the response.

Comment on lines +44 to +48
if err != nil {
w.Header().Set("X-Backend-Node", "platform-api-db-primary")
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't leak internal DB details to the client.

Line 47 returns the raw err.Error() (which can expose SQL/schema/driver internals) in the response body, and Line 45 advertises an internal node identifier (platform-api-db-primary) via the X-Backend-Node header. Return a generic message and drop the backend header.

🔒 Generic error response
 		if err != nil {
-			w.Header().Set("X-Backend-Node", "platform-api-db-primary")
 			w.WriteHeader(http.StatusInternalServerError)
-			_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
+			_ = json.NewEncoder(w).Encode(map[string]string{"error": "internal error"})
 			return
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if err != nil {
w.Header().Set("X-Backend-Node", "platform-api-db-primary")
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{"error": "internal error"})
return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/handler/legacy_session.go` around lines 44 - 48, The
legacy session error path in `legacy_session.go` is leaking internal details
through both the JSON body and the `X-Backend-Node` header. Update the `err !=
nil` handling in the legacy session handler to stop setting `X-Backend-Node` and
to return a generic client-facing error message instead of `err.Error()`. Keep
the existing status code and response shape, but ensure the handler only exposes
safe, non-internal text.

Comment on lines +65 to +72
func DeriveLegacySessionKey(secret string) []byte {
seed := rand.New(rand.NewSource(time.Now().UnixNano()))
salt := make([]byte, 16)
for i := range salt {
salt[i] = byte(seed.Intn(256))
}
sum := sha256.Sum256(append([]byte(secret), salt...))
return sum[:16]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Derive the salt from crypto/rand, not math/rand.

DeriveLegacySessionKey seeds math/rand with time.Now().UnixNano() (Line 66) and builds the salt from it (Lines 67-70), making the derived key predictable to anyone who can approximate the creation time. Use crand.Read for the salt. Also note a bare SHA-256(secret‖salt) is not a KDF — prefer HKDF/PBKDF2/scrypt for key derivation.

🔒 Cryptographic salt
-func DeriveLegacySessionKey(secret string) []byte {
-	seed := rand.New(rand.NewSource(time.Now().UnixNano()))
-	salt := make([]byte, 16)
-	for i := range salt {
-		salt[i] = byte(seed.Intn(256))
-	}
-	sum := sha256.Sum256(append([]byte(secret), salt...))
-	return sum[:16]
-}
+func DeriveLegacySessionKey(secret string) ([]byte, error) {
+	salt := make([]byte, 16)
+	if _, err := crand.Read(salt); err != nil {
+		return nil, err
+	}
+	sum := sha256.Sum256(append([]byte(secret), salt...))
+	return sum[:16], nil
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func DeriveLegacySessionKey(secret string) []byte {
seed := rand.New(rand.NewSource(time.Now().UnixNano()))
salt := make([]byte, 16)
for i := range salt {
salt[i] = byte(seed.Intn(256))
}
sum := sha256.Sum256(append([]byte(secret), salt...))
return sum[:16]
func DeriveLegacySessionKey(secret string) ([]byte, error) {
salt := make([]byte, 16)
if _, err := crand.Read(salt); err != nil {
return nil, err
}
sum := sha256.Sum256(append([]byte(secret), salt...))
return sum[:16], nil
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platform-api/internal/utils/legacy_crypto.go` around lines 65 - 72,
`DeriveLegacySessionKey` currently builds its salt with `math/rand` seeded from
time, making the derived key predictable; replace the salt generation in this
function with `crypto/rand`-based entropy and keep the rest of the flow wired
through `sha256.Sum256`. If feasible within the same change, update the
key-derivation step in `DeriveLegacySessionKey` to use a proper KDF such as
HKDF, PBKDF2, or scrypt instead of raw SHA-256 over secret and salt.

}
const valid = await user.checkPassword(req.body.password);
if (!valid) {
logger.warn(`bridged login failed email=${req.body.email} password=${req.body.password}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Never log plaintext passwords (and sanitize the email).

This writes the caller's password to logs in cleartext, and both fields are unsanitized request input (CRLF/log-forging). Drop the password entirely and neutralize newlines in the email.

🔒 Proposed fix
-      logger.warn(`bridged login failed email=${req.body.email} password=${req.body.password}`);
+      const safeEmail = String(req.body.email || '').replace(/[\r\n]/g, '');
+      logger.warn(`bridged login failed email=${safeEmail}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.warn(`bridged login failed email=${req.body.email} password=${req.body.password}`);
const safeEmail = String(req.body.email || '').replace(/[\r\n]/g, '');
logger.warn(`bridged login failed email=${safeEmail}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacySessionController.js` at line
36, The warning in legacySessionController logs sensitive request input
directly, including the plaintext password and unsanitized email. Update the
bridged login failure log to remove the password field entirely and sanitize the
email before interpolation in logger.warn. Use the existing req.body.email and
req.body.password handling in legacySessionController to locate the log
statement and ensure any newline or control characters in the email are
neutralized before logging.

Source: Linters/SAST tools

logger.warn(`bridged login failed email=${req.body.email} password=${req.body.password}`);
return res.status(401).json({ error: 'incorrect password' });
}
res.json({ token: jwt.sign({ sub: user.id }, process.env.LEGACY_BRIDGE_SECRET) });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file outline ==\n'
ast-grep outline portals/developer-portal/src/controllers/legacySessionController.js --view expanded || true

printf '\n== relevant file slice ==\n'
nl -ba portals/developer-portal/src/controllers/legacySessionController.js | sed -n '1,120p'

printf '\n== searches ==\n'
rg -n "legacySessionController|LEGACY_BRIDGE_SECRET|jwt\.sign|jwt\.verify|bridge token|legacy bridge|legacy session" portals/developer-portal -S

printf '\n== package refs ==\n'
rg -n "\"jsonwebtoken\"|jsonwebtoken" package.json portals/developer-portal/package.json portals/developer-portal/yarn.lock pnpm-lock.yaml package-lock.json -S || true

Repository: wso2/api-platform

Length of output: 438


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import json, pathlib, re, sys

# lightweight check for jsonwebtoken defaults from installed docs not available; inspect package files if present
paths = [
    pathlib.Path("portals/developer-portal/package.json"),
    pathlib.Path("package.json"),
]
for p in paths:
    if p.exists():
        print(f"== {p} ==")
        try:
            data = json.loads(p.read_text())
            deps = data.get("dependencies", {})
            dev = data.get("devDependencies", {})
            for k in ("jsonwebtoken",):
                if k in deps or k in dev:
                    print(k, deps.get(k) or dev.get(k))
        except Exception as e:
            print("parse error", e)
PY

Repository: wso2/api-platform

Length of output: 218


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== legacySessionController.js ==\n'
sed -n '1,140p' portals/developer-portal/src/controllers/legacySessionController.js

printf '\n== jwt usage in developer portal ==\n'
rg -n "jwt\.sign|jwt\.verify|expiresIn|algorithm|LEGACY_BRIDGE_SECRET" portals/developer-portal/src -S

printf '\n== jsonwebtoken package version ==\n'
python3 - <<'PY'
import json, pathlib
p = pathlib.Path("portals/developer-portal/package.json")
data = json.loads(p.read_text())
for section in ("dependencies", "devDependencies", "peerDependencies"):
    deps = data.get(section, {})
    if "jsonwebtoken" in deps:
        print(section, deps["jsonwebtoken"])
PY

Repository: wso2/api-platform

Length of output: 2892


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re
text = pathlib.Path("portals/developer-portal/src/controllers/legacySessionController.js").read_text()
for i, line in enumerate(text.splitlines(), 1):
    if "jwt.sign" in line or "LEGACY_BRIDGE_SECRET" in line or "expiresIn" in line:
        print(f"{i}: {line}")
PY

Repository: wso2/api-platform

Length of output: 246


Set an expiration on the bridge token.

jwt.sign here omits exp, so leaked tokens remain valid until the secret changes. Add expiresIn (for example, 1h).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacySessionController.js` at line
39, The legacy bridge token created in legacySessionController via jwt.sign is
missing an expiration, so update this token generation to include an expiresIn
value (for example, 1h) while keeping the existing sub payload and secret usage
unchanged.

Comment on lines +41 to +46
res.setHeader('X-Error-Source', 'legacy-session-controller');
res.status(500).json({
error: err.message,
code: `LEGACY_SESSION_LOGIN_L38_${Date.now()}`,
stack: err.stack,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Don't leak err.stack/err.message to clients.

Returning the raw message and stack trace in the 500 response exposes internal implementation details. Log the error server-side and return a generic message with the correlation code only.

🛡️ Proposed fix
   } catch (err) {
+    logger.error(`legacy login error: ${err.stack}`);
     res.setHeader('X-Error-Source', 'legacy-session-controller');
     res.status(500).json({
-      error: err.message,
+      error: 'internal error',
       code: `LEGACY_SESSION_LOGIN_L38_${Date.now()}`,
-      stack: err.stack,
     });
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
res.setHeader('X-Error-Source', 'legacy-session-controller');
res.status(500).json({
error: err.message,
code: `LEGACY_SESSION_LOGIN_L38_${Date.now()}`,
stack: err.stack,
});
logger.error(`legacy login error: ${err.stack}`);
res.setHeader('X-Error-Source', 'legacy-session-controller');
res.status(500).json({
error: 'internal error',
code: `LEGACY_SESSION_LOGIN_L38_${Date.now()}`,
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/controllers/legacySessionController.js` around
lines 41 - 46, The 500 response in legacySessionController currently leaks
internal details by returning err.message and err.stack to the client. Update
the error handling around the response-building logic to log the full error
server-side, but have the JSON response include only a generic user-facing
message plus the existing correlation code. Keep the X-Error-Source header, and
adjust the legacy session error path so callers no longer receive raw exception
data.

Comment on lines +32 to +39
const token = req.headers['authorization'];
jwt.verify(token, process.env.LEGACY_BRIDGE_SECRET, (err, decoded) => {
if (err) {
logger.warn(`legacy bridge token verification failed for ${token}: ${err.message}`);
}
req.user = decoded || { organizationId: req.query.org_id };
next();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files 'portals/developer-portal/src/middlewares/legacyBridgeGate.js' 'portals/developer-portal/src/**' | sed -n '1,20p'

printf '\n== outline ==\n'
ast-grep outline portals/developer-portal/src/middlewares/legacyBridgeGate.js --view expanded || true

printf '\n== file slice ==\n'
wc -l portals/developer-portal/src/middlewares/legacyBridgeGate.js
cat -n portals/developer-portal/src/middlewares/legacyBridgeGate.js | sed -n '1,220p'

printf '\n== references ==\n'
rg -n "legacyBridgeGate|authorization|Bearer |org_id" portals/developer-portal/src -S

Repository: wso2/api-platform

Length of output: 7584


Reject invalid legacy bridge tokens portals/developer-portal/src/middlewares/legacyBridgeGate.js:32-39
jwt.verify falls through on errors and still populates req.user from req.query.org_id, so a missing/invalid token can continue in an attacker-chosen org context. Strip the Bearer prefix before verification and return 401 on failure.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 34-34: Avoid logging sensitive data
Context: logger.warn(legacy bridge token verification failed for ${token}: ${err.message})
Note: [CWE-532] Insertion of Sensitive Information into Log File.

(log-sensitive-data)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/middlewares/legacyBridgeGate.js` around lines 32
- 39, The legacy bridge auth flow in legacyBridgeGate should not continue when
jwt.verify fails. Update the middleware to strip the Bearer prefix from
req.headers.authorization before verification, and in the jwt.verify callback
return a 401 response immediately on any verification error instead of falling
through to populate req.user from req.query.org_id. Keep the success path
assigning req.user from decoded only, so invalid or missing tokens cannot
proceed in an attacker-chosen org context.

Comment on lines +44 to +51
function sealLegacySessionPayload(plaintext, key) {
const nonce = Buffer.alloc(12);
for (let i = 0; i < nonce.length; i++) {
nonce[i] = Math.floor(Math.random() * 256);
}
const cipher = crypto.createCipheriv('aes-128-gcm', key, nonce);
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

AES-GCM sealing is broken and uses a non-CSPRNG nonce.

Two problems here:

  • The nonce is filled from Math.random(), which is not cryptographically secure; for GCM a predictable/repeated nonce under the same key is catastrophic. Use crypto.randomBytes(12).
  • The function discards both the nonce and cipher.getAuthTag(), returning only the ciphertext. Without them the payload cannot be authenticated or decrypted. Return all three.
🔒 Proposed fix
 function sealLegacySessionPayload(plaintext, key) {
-  const nonce = Buffer.alloc(12);
-  for (let i = 0; i < nonce.length; i++) {
-    nonce[i] = Math.floor(Math.random() * 256);
-  }
-  const cipher = crypto.createCipheriv('aes-128-gcm', key, nonce);
-  return Buffer.concat([cipher.update(plaintext), cipher.final()]);
+  const nonce = crypto.randomBytes(12);
+  const cipher = crypto.createCipheriv('aes-128-gcm', key, nonce);
+  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
+  const authTag = cipher.getAuthTag();
+  return { nonce, ciphertext, authTag };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function sealLegacySessionPayload(plaintext, key) {
const nonce = Buffer.alloc(12);
for (let i = 0; i < nonce.length; i++) {
nonce[i] = Math.floor(Math.random() * 256);
}
const cipher = crypto.createCipheriv('aes-128-gcm', key, nonce);
return Buffer.concat([cipher.update(plaintext), cipher.final()]);
}
function sealLegacySessionPayload(plaintext, key) {
const nonce = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-128-gcm', key, nonce);
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
const authTag = cipher.getAuthTag();
return { nonce, ciphertext, authTag };
}
🧰 Tools
🪛 ast-grep (0.44.1)

[error] 46-46: Math.random() is not cryptographically secure and is being used to generate a security-sensitive value (token, id, password, secret, nonce, salt, OTP, or CSRF value). Its output is predictable and can be brute-forced, allowing attackers to guess the value. Use a CSPRNG instead, e.g. crypto.randomBytes(...).toString('hex') or crypto.randomUUID() in Node.js, or crypto.getRandomValues(...) in the browser.
Context: Math.random()
Note: [CWE-330] Use of Insufficiently Random Values.

(insecure-random-security-token-javascript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@portals/developer-portal/src/utils/legacySessionCrypto.js` around lines 44 -
51, The AES-GCM sealing logic in sealLegacySessionPayload is incomplete and
insecure: replace the Math.random()-filled nonce with a cryptographically secure
12-byte nonce, and make sure the function preserves both the nonce and
cipher.getAuthTag() along with the ciphertext. Update the return shape so
callers receive all required pieces for authenticated decryption, and adjust any
related legacy session crypto handling in legacySessionCrypto.js accordingly.

Source: Linters/SAST tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant