-
Notifications
You must be signed in to change notification settings - Fork 23
User Update Internal access api - service-to-service calls #872
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -116,6 +116,11 @@ let enviromentVariables = { | |
| optional: true, | ||
| default: 86400, | ||
| }, | ||
| INTERNAL_ACCESS_TOKEN: { | ||
| message: | ||
| 'Secret token for internal/service-to-service and data-pipeline API calls (e.g. user update without user auth token). Keep secure.', | ||
| optional: true, | ||
| }, | ||
|
Comment on lines
+119
to
+123
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical: In Either make this variable required ( Option A – make it required INTERNAL_ACCESS_TOKEN: {
message:
'Secret token for internal/service-to-service and data-pipeline API calls (e.g. user update without user auth token). Keep secure.',
- optional: true,
+ optional: false,
},Option B – guard in the authenticator (src/middlewares/authenticator.js)- if (req.headers.internal_access_token === process.env.INTERNAL_ACCESS_TOKEN) return true
+ if (process.env.INTERNAL_ACCESS_TOKEN && req.headers.internal_access_token === process.env.INTERNAL_ACCESS_TOKEN) return true🤖 Prompt for AI Agents |
||
| REDIS_HOST: { | ||
| message: 'Redis Host Url', | ||
| optional: false, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,21 +21,36 @@ const tenantDomainQueries = require('@database/queries/tenantDomain') | |
| const organizationQueries = require('@database/queries/organization') | ||
|
|
||
| async function checkPermissions(roleTitle, requestPath, requestMethod) { | ||
| const parts = requestPath.match(/[^/]+/g) | ||
| const api_path = [`/${parts[0]}/${parts[1]}/${parts[2]}/*`] | ||
|
|
||
| if (parts[4]) api_path.push(`/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*`) | ||
| else | ||
| api_path.push( | ||
| `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}`, | ||
| `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*` | ||
| ) | ||
| const parts = requestPath.match(/[^/]+/g) || [] | ||
| let api_path | ||
|
|
||
| if (parts.length >= 4) { | ||
| api_path = [`/${parts[0]}/${parts[1]}/${parts[2]}/*`] | ||
| if (parts[4]) api_path.push(`/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*`) | ||
| else | ||
| api_path.push( | ||
| `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}`, | ||
| `/${parts[0]}/${parts[1]}/${parts[2]}/${parts[3]}*` | ||
| ) | ||
| } else if (parts.length === 3) { | ||
| api_path = [`/${parts[0]}/${parts[1]}/${parts[2]}`, `/${parts[0]}/${parts[1]}/${parts[2]}/*`] | ||
| } else if (parts.length === 2) { | ||
| api_path = [`/${parts[0]}/${parts[1]}`, `/${parts[0]}/${parts[1]}/*`] | ||
| } else if (parts.length === 1) { | ||
| api_path = [`/${parts[0]}`, `/${parts[0]}/*`] | ||
| } else { | ||
| return false | ||
| } | ||
|
|
||
| if (Array.isArray(roleTitle) && !roleTitle.includes(common.PUBLIC_ROLE)) { | ||
| roleTitle.push(common.PUBLIC_ROLE) | ||
| } | ||
|
|
||
| const filter = { role_title: roleTitle, module: parts[2], api_path: { [Op.in]: api_path } } | ||
| const filter = { | ||
| role_title: roleTitle, | ||
| ...(parts[2] != null && { module: parts[2] }), | ||
| api_path: { [Op.in]: api_path }, | ||
| } | ||
| const attributes = ['request_type', 'api_path', 'module'] | ||
| const allowedPermissions = await rolePermissionMappingQueries.findAll(filter, attributes) | ||
|
|
||
|
|
@@ -98,7 +113,53 @@ module.exports = async function (req, res, next) { | |
| } | ||
| }) | ||
|
|
||
| if (internalAccess && !authHeader) return next() | ||
| // Internal access without user token (existing internal URLs unchanged; pipeline uses internal_access_token only) | ||
| if (internalAccess && !authHeader) { | ||
| // New path only: data-pipeline user update — resolve target user from headers, set decodedToken; no change to other internal URLs | ||
| const internalUserUpdatePath = '/user/v1/user/update' | ||
| if (req.path.includes(internalUserUpdatePath)) { | ||
| // Express normalizes headers to lowercase | ||
| const userId = (req.headers[common.INTERNAL_USER_ID_HEADER] || '').trim() | ||
| const tenantCode = (req.headers[common.TENANT_CODE_HEADER] || req.headers['x-tenant-code'] || '').trim() | ||
| if (!userId || !tenantCode) { | ||
| throw responses.failureResponse({ | ||
| message: 'INTERNAL_UPDATE_REQUIRES_USER_ID_AND_TENANT', | ||
| statusCode: httpStatusCode.bad_request, | ||
| responseCode: 'CLIENT_ERROR', | ||
| }) | ||
| } | ||
| const user = await userQueries.findUserWithOrganization( | ||
| { id: userId, tenant_code: tenantCode }, | ||
| {}, | ||
| false | ||
| ) | ||
| if (!user) { | ||
| throw responses.failureResponse({ | ||
| message: 'USER_NOT_FOUND', | ||
| statusCode: httpStatusCode.unauthorized, | ||
| responseCode: 'UNAUTHORIZED', | ||
| }) | ||
| } | ||
| const org = user.organizations?.[0] | ||
| if (!org) { | ||
| throw responses.failureResponse({ | ||
| message: 'USER_HAS_NO_ORGANIZATION', | ||
| statusCode: httpStatusCode.bad_request, | ||
| responseCode: 'CLIENT_ERROR', | ||
| }) | ||
| } | ||
| req.decodedToken = { | ||
| id: user.id, | ||
| name: user.name, | ||
| session_id: null, | ||
| tenant_code: user.tenant_code, | ||
| organization_id: org.id, | ||
| organization_code: org.code, | ||
| roles: org.roles || [], | ||
| } | ||
| } | ||
| return next() | ||
| } | ||
|
Comment on lines
+116
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# First, find and examine the authenticator.js file around the specified lines
fd -t f "authenticator.js" | head -5Repository: ELEVATE-Project/user Length of output: 96 🏁 Script executed: # Check the file structure and get context around lines 116-162
cat -n src/middlewares/authenticator.js | sed -n '110,170p'Repository: ELEVATE-Project/user Length of output: 2556 🏁 Script executed: # Look for common.TENANT_CODE_HEADER definition
rg -n "TENANT_CODE_HEADER" --type=js -B2 -A2Repository: ELEVATE-Project/user Length of output: 4617 🏁 Script executed: # Check session_id usage around line 239 and in general
rg -n "session_id" src/middlewares/authenticator.js -B2 -A2Repository: ELEVATE-Project/user Length of output: 620 🏁 Script executed: # Find and examine findUserWithOrganization function
fd -t f "*.js" | xargs rg -l "findUserWithOrganization" | head -5Repository: ELEVATE-Project/user Length of output: 490 🏁 Script executed: # Check for permission checks in the middleware
rg -n "checkPermission" src/middlewares/authenticator.js -B3 -A3Repository: ELEVATE-Project/user Length of output: 855 🏁 Script executed: # Find and examine findUserWithOrganization function definition
rg -n "findUserWithOrganization" -A 10 --type=js | head -40Repository: ELEVATE-Project/user Length of output: 2366 🏁 Script executed: # Check how userId/id is typically validated elsewhere in the codebase
rg -n "parseInt.*userId|UUID.*userId|validate.*userId" -B2 -A2 --type=js | head -50Repository: ELEVATE-Project/user Length of output: 46 🏁 Script executed: # Look at the userQueries to see the function signature
find src -name "*queries*" -o -name "*user*" | grep -i queryRepository: ELEVATE-Project/user Length of output: 46 🏁 Script executed: # Check context around line 239 to understand if the internal path actually reaches that code
cat -n src/middlewares/authenticator.js | sed -n '225,245p'Repository: ELEVATE-Project/user Length of output: 861 🏁 Script executed: # Verify if common.INTERNAL_USER_ID_HEADER is defined and what it contains
rg -n "INTERNAL_USER_ID_HEADER" --type=js -B2 -A2Repository: ELEVATE-Project/user Length of output: 951 🏁 Script executed: # Look at the userQueries file to see findUserWithOrganization implementation
find src -path "*database*" -name "*.js" | xargs rg -l "findUserWithOrganization" 2>/dev/null | head -3Repository: ELEVATE-Project/user Length of output: 93 🏁 Script executed: # Check database queries to see how id is used in findUserWithOrganization
rg -n "findUserWithOrganization" -A 15 src/database/queries/user.js 2>/dev/null | head -50Repository: ELEVATE-Project/user Length of output: 46 🏁 Script executed: # Check if there are other places where userId from headers is validated
rg -n "INTERNAL_USER_ID_HEADER\|x-internal-user-id" -B5 -A5 --type=jsRepository: ELEVATE-Project/user Length of output: 46 🏁 Script executed: # Verify the control flow - does the internal path actually reach line 239?
cat -n src/middlewares/authenticator.js | sed -n '115,165p'Repository: ELEVATE-Project/user Length of output: 2118 🏁 Script executed: # Try to find and read the users.js file in database queries
find src -name "users.js" -o -name "user.js" | grep -i databaseRepository: ELEVATE-Project/user Length of output: 122 🏁 Script executed: # Get the actual findUserWithOrganization implementation
rg -n "findUserWithOrganization\s*=" src/database/queries/users.js -A 20 2>/dev/null || rg -n "findUserWithOrganization" src/database/queries/users.js -A 20Repository: ELEVATE-Project/user Length of output: 757 🏁 Script executed: # Check if there's validation elsewhere in the middleware or at the entry point for userId
rg -n "INTERNAL_USER_ID_HEADER" -B10 -A10 --type=jsRepository: ELEVATE-Project/user Length of output: 3059 🏁 Script executed: # Verify the complete control flow - check if line 239 is reachable from the internal path
cat -n src/middlewares/authenticator.js | sed -n '115,250p'Repository: ELEVATE-Project/user Length of output: 5581 🏁 Script executed: # Verify if /user/v1/user/update-profile or similar would match req.path.includes()
node <<'EOF'
const path1 = '/user/v1/user/update';
const path2 = '/user/v1/user/update';
const path3 = '/user/v1/user/update-profile';
const path4 = '/user/v1/user/update/extra';
console.log('path.includes test:');
console.log(`path2 includes path1: ${path2.includes(path1)}`);
console.log(`path3 includes path1: ${path3.includes(path1)}`);
console.log(`path4 includes path1: ${path4.includes(path1)}`);
EOFRepository: ELEVATE-Project/user Length of output: 164 🏁 Script executed: # Check what happens if userId is invalid - look at Sequelize's behavior with where clause
rg -n "where:" src/database/queries/users.js -A 5 | head -20Repository: ELEVATE-Project/user Length of output: 390 🏁 Script executed: # Confirm that the internal path returns before reaching line 239 by checking the flow
cat -n src/middlewares/authenticator.js | sed -n '160,165p'Repository: ELEVATE-Project/user Length of output: 162 Multiple security concerns in the internal-access update flow.
Proposed fixes for items 1–3- if (req.path.includes(internalUserUpdatePath)) {
+ if (req.path === internalUserUpdatePath || req.path.startsWith(internalUserUpdatePath + '/')) {
// Express normalizes headers to lowercase
const userId = (req.headers[common.INTERNAL_USER_ID_HEADER] || '').trim()
- const tenantCode = (req.headers[common.TENANT_CODE_HEADER] || req.headers['x-tenant-code'] || '').trim()
- if (!userId || !tenantCode) {
+ const tenantCode = (req.headers[common.TENANT_CODE_HEADER] || '').trim()
+ if (!userId || !tenantCode || !/^\d+$/.test(userId)) {
throw responses.failureResponse({
message: 'INTERNAL_UPDATE_REQUIRES_USER_ID_AND_TENANT',
statusCode: httpStatusCode.bad_request,
responseCode: 'CLIENT_ERROR',
})
}🤖 Prompt for AI Agents |
||
|
|
||
| if (!authHeader) { | ||
| try { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exposing
/user/v1/user/updateas an internal-access URL is a significant privilege escalation surface — ensure the token bypass issue is fixed first.This endpoint allows modifying user data. Combined with the
undefined === undefinedbypass noted inenvVariables.js, any caller can hit this endpoint without authentication if the env var is unset. The constants themselves are fine once the token validation is hardened.🤖 Prompt for AI Agents