Skip to content

Feature/sec101#455

Closed
alexjavabraz wants to merge 10 commits intomainfrom
feature/sec101
Closed

Feature/sec101#455
alexjavabraz wants to merge 10 commits intomainfrom
feature/sec101

Conversation

@alexjavabraz
Copy link
Collaborator

No description provided.

@alexjavabraz alexjavabraz self-assigned this Feb 12, 2026
Copilot AI review requested due to automatic review settings February 12, 2026 21:19
@github-actions
Copy link

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails

Scanned Files

@sonarqubecloud
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a request security layer to the LoopBack app by enforcing an API key + payload-hash check, and introduces response caching keyed by request + payload hash to allow replaying identical requests without re-executing handlers.

Changes:

  • Added requestSecurityMiddleware to validate api_key and x-payload-hash, and to cache/replay responses.
  • Updated application bootstrap to register the new middleware and to configure CORS origins/headers.
  • Removed the /logs controller and added acceptance coverage for the new middleware behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/middleware/request-security.middleware.ts Implements API key + payload hash verification and response caching/replay.
src/application.ts Registers the new middleware and adds CORS configuration driven by CORS_ORIGIN.
src/controllers/logs.controller.ts Removes the /logs endpoint implementation.
src/tests/unit/api-information.controller.unit.ts Minor unit test structure/format changes.
src/tests/acceptance/request-security.acceptance.ts Adds acceptance tests for request security + caching.

...options,
rest: {
cors: {
origin: [corsOrigins],
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

cors.origin is being set to [corsOrigins], which produces a nested array (string[][]). LoopBack/Express CORS expects a string or string[], so this will likely prevent origin matching and break CORS. Use origin: corsOrigins (or a single string) instead.

Suggested change
origin: [corsOrigins],
origin: corsOrigins,

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +55
this.middleware(requestSecurityMiddleware, {
group: 'requestSecurity',
upstreamGroups: RestMiddlewareGroups.PARSE_PARAMS,
downstreamGroups: RestMiddlewareGroups.INVOKE_METHOD,
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The request security middleware is registered unconditionally for the whole app, but requestSecurityMiddleware throws 500 when REQUEST_API_KEY/REQUEST_SALT are not set. Existing acceptance tests (and local dev) start the app without those env vars (see setupApplication()), so all requests will fail. Consider enabling this middleware only when the env vars are present / in specific environments, or updating the shared test bootstrap to set the required env vars.

Suggested change
this.middleware(requestSecurityMiddleware, {
group: 'requestSecurity',
upstreamGroups: RestMiddlewareGroups.PARSE_PARAMS,
downstreamGroups: RestMiddlewareGroups.INVOKE_METHOD,
});
if (process.env.REQUEST_API_KEY && process.env.REQUEST_SALT) {
this.middleware(requestSecurityMiddleware, {
group: 'requestSecurity',
upstreamGroups: RestMiddlewareGroups.PARSE_PARAMS,
downstreamGroups: RestMiddlewareGroups.INVOKE_METHOD,
});
}

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +220
if (result === undefined) {
const contentType = getContentType(response.getHeader('content-type'));

storeCachedResponse(cacheKey, {
type: 'raw',
statusCode: response.statusCode,
contentType,
payload: concatChunks(responseChunks),
});

return result;
}

storeCachedResponse(cacheKey, {
type: 'result',
result,
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Caching result when it is non-undefined is unsafe because many controllers return the Express Response object (after calling res.send()), which is tied to a specific request and cannot be reused. This will cache and later return a stale Response instance instead of replaying bytes, breaking cached responses for endpoints that return Promise<Response>. Prefer caching/replaying the captured raw response when the response has been written/ended (e.g., response.headersSent / response.writableEnded or result === response) and avoid storing Response objects in the cache.

Copilot uses AI. Check for mistakes.
Comment on lines +121 to +149
if (!expectedApiKey) {
throw new HttpErrors.InternalServerError();
}

if (!rootstockSalt) {
throw new HttpErrors.InternalServerError();
}

const providedApiKey = request.header(API_KEY_HEADER);
if (!providedApiKey) {
throw new HttpErrors.Unauthorized();
}

if (!secureStringCompare(providedApiKey, expectedApiKey)) {
throw new HttpErrors.Unauthorized();
}

const providedPayloadHash = request.header(PAYLOAD_HASH_HEADER);
if (!providedPayloadHash) {
throw new HttpErrors.Unauthorized();
}

const normalizedPayloadHash = providedPayloadHash.toLowerCase();
const payload = getRequestPayload(request);
const expectedPayloadHash = sha256(sha256(payload + rootstockSalt));

if (!secureStringCompare(normalizedPayloadHash, expectedPayloadHash)) {
throw new HttpErrors.Unauthorized();
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

All Unauthorized/InternalServerError errors are thrown without messages. The new acceptance tests assert specific messages (e.g., missing api_key vs invalid api key), and without explicit messages LoopBack will return the generic "Unauthorized" text. Provide explicit error messages (and ideally differentiate missing vs invalid headers) so clients/tests can reliably understand failures.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +191
appendChunk(chunk, args[0] as BufferEncoding | undefined);
return originalWrite(chunk as never, ...(args as never[]));
}) as typeof response.write;

response.end = ((chunk?: unknown, ...args: unknown[]) => {
appendChunk(chunk, args[0] as BufferEncoding | undefined);
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The response.write/response.end wrappers assume args[0] is always a BufferEncoding, but Node allows passing a callback as the second argument (e.g., res.end('ok', cb)). In that case Buffer.from(chunk, encoding) will receive a function and can throw. Parse the overloads safely (only treat args[0] as encoding when it’s a string) before calling Buffer.from.

Suggested change
appendChunk(chunk, args[0] as BufferEncoding | undefined);
return originalWrite(chunk as never, ...(args as never[]));
}) as typeof response.write;
response.end = ((chunk?: unknown, ...args: unknown[]) => {
appendChunk(chunk, args[0] as BufferEncoding | undefined);
const encoding =
typeof args[0] === 'string' ? (args[0] as BufferEncoding) : undefined;
appendChunk(chunk, encoding);
return originalWrite(chunk as never, ...(args as never[]));
}) as typeof response.write;
response.end = ((chunk?: unknown, ...args: unknown[]) => {
let actualChunk = chunk;
let encoding: BufferEncoding | undefined;
// Handle res.end(cb) overload: first argument is a callback, no body.
if (typeof actualChunk === 'function') {
actualChunk = undefined;
} else if (typeof args[0] === 'string') {
encoding = args[0] as BufferEncoding;
}
appendChunk(actualChunk, encoding);

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +141

it('replays cached response for repeated /logs payload hash', async () => {
const body = {
type: 'error',
operation: 'peginNative',
location: 'frontend register',
error: {
message: 'wallet timeout',
code: 504,
},
};
const payloadHash = hashJsonPayload(body);

await client
.post('/logs')
.set('api_key', apiKey)
.set('x-payload-hash', payloadHash)
.send(body)
.expect(200);

await client
.post('/logs')
.set('api_key', apiKey)
.set('x-payload-hash', payloadHash)
.send(body)
.expect(200)
.expect('x-payload-hash-cache', 'HIT');
});
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This test posts to /logs, but src/controllers/logs.controller.ts has been removed in this PR, so the route will return 404 and this caching assertion won’t be valid. Either restore/replace the /logs endpoint or update/remove this test to target an existing route.

Suggested change
it('replays cached response for repeated /logs payload hash', async () => {
const body = {
type: 'error',
operation: 'peginNative',
location: 'frontend register',
error: {
message: 'wallet timeout',
code: 504,
},
};
const payloadHash = hashJsonPayload(body);
await client
.post('/logs')
.set('api_key', apiKey)
.set('x-payload-hash', payloadHash)
.send(body)
.expect(200);
await client
.post('/logs')
.set('api_key', apiKey)
.set('x-payload-hash', payloadHash)
.send(body)
.expect(200)
.expect('x-payload-hash-cache', 'HIT');
});

Copilot uses AI. Check for mistakes.
@alexjavabraz alexjavabraz deleted the feature/sec101 branch February 13, 2026 00:21
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.

2 participants