diff --git a/.env.example b/.env.example index 566e5b81141..3e4478a3ce2 100644 --- a/.env.example +++ b/.env.example @@ -7,12 +7,13 @@ PORT=3000 SERVER_URL=http://localhost:3000 DEFAULT_LOCALE=ru FILE_FIELD_ADAPTER=local -GOOGLE_RECAPTCHA_CONFIG='{"SITE_KEY":"6LcPRvQaAAAAAJRyxsFIB4rP5VH036pFOkNH8lgh", "SERVER_KEY":"6LcPRvQaAAAAADn_h1440Es7fXIGD0E4lpXR_FyF"}' +# nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key +GOOGLE_RECAPTCHA_CONFIG='{"SITE_KEY":"", "SERVER_KEY":""}' HELP_REQUISITES='{ "support_email": "help@example.com", "support_email_mobile": "helpmobile@example.com", "bot_email": "service@example.com", "support_phone": "+1 301 000-00-00" }' # Cache settings -ADAPTER_CACHE_CONFIG = '{ "enabled": true, "excludedLists":[], "logging":0, "maxCacheSize":1000, "logStatsEachSecs": 60 }' -REQUEST_CACHE_CONFIG = '{ "enabled": true, "logging":0, "logStatsEachSecs": 60 }' +ADAPTER_CACHE_CONFIG='{ "enabled": true, "excludedLists":[], "logging":0, "maxCacheSize":1000, "logStatsEachSecs": 60 }' +REQUEST_CACHE_CONFIG='{ "enabled": true, "logging":0, "logStatsEachSecs": 60 }' #OIDC_CONDO_CLIENT_CONFIG='{"serverUrl":"http://localhost:3000", "clientId":"", "clientSecret":""}' @@ -24,10 +25,11 @@ FEATURE_TOGGLE_CONFIG='{"url":null,"apiKey":null,"static":{}}' BANK_ACCOUNT_REQUEST_EMAIL_TARGET=help@doma.ai # FIREBASE_CONFIG_JSON contents can be retrieved from https://console.firebase.google.com/project//settings/serviceaccounts/adminsdk +# nosemgrep: generic.secrets.security.detected-google-gcm-service-account.detected-google-gcm-service-account # FIREBASE_CONFIG_JSON='{"type": "service_account", "project_id": "", "private_key_id": "", "client_email": " at .gserviceaccount.com", "client_id": "", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/"}' # real Firebase push token for testing real push notification delivery to device, would be used only if provided -# FIREBASE_PUSH_TOKEN_TEST=flt0weSOS8eROf6OEZAmHp:APA91bG0I-QBvGjCL5jblzDoOuVV6bZ1x4dyRmyPqr2iZBYyFbDJcqtKw0vvzo4MH-PaLiVQJbLfAGCoryYXykdqCKXdx2QqYAk6vE4pmxrKk6RHe33-mVNHNTB0HxYI9KUsb21CHfPp +# FIREBASE_PUSH_TOKEN_TEST= # HMS - Huawei Messaging System # HMS_CONFIG_JSON contents can be retrieved from ..., there should be separate section for each app receiving push notifications via HMS diff --git a/.github/workflows/nodejs.condo.code.analysis.yml b/.github/workflows/nodejs.condo.code.analysis.yml index aaaa21cd1d9..29bdfe35b21 100644 --- a/.github/workflows/nodejs.condo.code.analysis.yml +++ b/.github/workflows/nodejs.condo.code.analysis.yml @@ -23,9 +23,10 @@ jobs: steps: # Fetch project source with GitHub Actions Checkout. - uses: actions/checkout@v3 - - run: ./bin/run-semgrep.sh + - run: ./bin/run-semgrep.sh -s - name: Upload SARIF file for GitHub Advanced Security Dashboard uses: github/codeql-action/upload-sarif@v2 with: - sarif_file: semgrep.sarif + sarif_file: ./semgrep_results + if: always() diff --git a/.gitignore b/.gitignore index 8315c457973..703f057f65a 100644 --- a/.gitignore +++ b/.gitignore @@ -59,4 +59,5 @@ apps/condo/public/ui .turbo # semgrep -semgrep.sarif \ No newline at end of file +semgrep.sarif +semgrep_results/* \ No newline at end of file diff --git a/.semgrepignore b/.semgrepignore index 71ebed70145..f1faa8cd1ea 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -21,3 +21,7 @@ yarn-error.log npm-debug.* ignore.* + +# not a source code files +docker-compose.yml +packages/codegen/* \ No newline at end of file diff --git a/apps/address-service/domains/common/oidc.js b/apps/address-service/domains/common/oidc.js index 0a47e319ffb..f13e865b57b 100644 --- a/apps/address-service/domains/common/oidc.js +++ b/apps/address-service/domains/common/oidc.js @@ -58,6 +58,8 @@ class OIDCHelper { class OIDCKeystoneApp { prepareMiddleware ({ keystone, distDir, dev }) { + // this route can not be used for csrf attack (use oidc-client library to handle auth flows properly) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() const oidcSessionKey = 'oidc' const helper = new OIDCHelper() diff --git a/apps/address-service/domains/common/utils/services/search/SearchKeystoneApp.js b/apps/address-service/domains/common/utils/services/search/SearchKeystoneApp.js index df22224159e..7262f09fb35 100644 --- a/apps/address-service/domains/common/utils/services/search/SearchKeystoneApp.js +++ b/apps/address-service/domains/common/utils/services/search/SearchKeystoneApp.js @@ -48,6 +48,8 @@ class SearchKeystoneApp { let keystoneContext + // this route can not be used for csrf attack (because no cookies and tokens are used in a public route) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() const addressParser = new AddressFromStringParser() diff --git a/apps/address-service/domains/common/utils/services/search/searchServiceUtils.js b/apps/address-service/domains/common/utils/services/search/searchServiceUtils.js index d0c3703890f..f1c621c8c35 100644 --- a/apps/address-service/domains/common/utils/services/search/searchServiceUtils.js +++ b/apps/address-service/domains/common/utils/services/search/searchServiceUtils.js @@ -1,8 +1,7 @@ -const { createHash } = require('crypto') - const { isEmpty, isObject } = require('lodash') const { AddressSource } = require('@address-service/domains/address/utils/serverSchema') +const { md5 } = require('@condo/domains/common/utils/crypto') /** * @param context Keystone context @@ -114,8 +113,7 @@ function sortObject (obj) { function hashJSON (obj) { const sortedObj = sortObject(obj) const jsonStr = JSON.stringify(sortedObj) - const hash = createHash('md5') - return hash.update(jsonStr).digest('hex') + return md5(jsonStr) } /** diff --git a/apps/address-service/domains/common/utils/services/suggest/SuggestionKeystoneApp.js b/apps/address-service/domains/common/utils/services/suggest/SuggestionKeystoneApp.js index 293dc32e667..18e6853e126 100644 --- a/apps/address-service/domains/common/utils/services/suggest/SuggestionKeystoneApp.js +++ b/apps/address-service/domains/common/utils/services/suggest/SuggestionKeystoneApp.js @@ -46,6 +46,8 @@ class SuggestionKeystoneApp { * @returns {Express} */ prepareMiddleware (params) { + // this route can not be used for csrf attack (a public route) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() function setNoCache (req, res, next) { diff --git a/apps/address-service/index.js b/apps/address-service/index.js index 11ee7f15a0b..a715fd353ed 100644 --- a/apps/address-service/index.js +++ b/apps/address-service/index.js @@ -104,6 +104,9 @@ module.exports = { const requestIdHeaderName = 'X-Request-Id' app.use(function reqId (req, res, next) { const reqId = req.headers[requestIdHeaderName.toLowerCase()] || v4() + // we are expecting to receive reqId from client in order to have fully traced logs end to end + // also, property name are constant name, not a dynamic user input + // nosemgrep: javascript.express.security.audit.remote-property-injection.remote-property-injection req['id'] = req.headers[requestIdHeaderName.toLowerCase()] = reqId res.setHeader(requestIdHeaderName, reqId) next() diff --git a/apps/address-service/initialData.js b/apps/address-service/initialData.js index 5e206bd7069..f346884784b 100644 --- a/apps/address-service/initialData.js +++ b/apps/address-service/initialData.js @@ -8,6 +8,8 @@ module.exports = [ email: 'admin@example.com', isAdmin: true, isSupport: true, + // this is development only data + // nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key password: '3a74b3f07978', dv: 1, sender: { @@ -22,6 +24,8 @@ module.exports = [ email: 'user@example.com', isAdmin: false, isSupport: false, + // this is development only data + // nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key password: '1a92b3a07c78', dv: 1, sender: { diff --git a/apps/condo/domains/acquiring/PaymentLinkMiddleware.js b/apps/condo/domains/acquiring/PaymentLinkMiddleware.js index a2d8ad3146e..80939954568 100644 --- a/apps/condo/domains/acquiring/PaymentLinkMiddleware.js +++ b/apps/condo/domains/acquiring/PaymentLinkMiddleware.js @@ -5,6 +5,8 @@ const { PaymentLinkRouter } = require('@condo/domains/acquiring/routes/paymentLi class PaymentLinkMiddleware { async prepareMiddleware () { + // this route can not be used for csrf attack (because no cookies and tokens are used in a public route) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() const router = new PaymentLinkRouter() diff --git a/apps/condo/domains/banking/constants.js b/apps/condo/domains/banking/constants.js index 20212b20191..89df5d1a257 100644 --- a/apps/condo/domains/banking/constants.js +++ b/apps/condo/domains/banking/constants.js @@ -4,6 +4,8 @@ const BANK_INTEGRATION_IDS = { SBBOL: 'd94743b0-e5d5-4d06-a244-ea4b2edb8633', + // not a credential + // nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key '1CClientBankExchange': '61e3d767-bd62-40e3-a503-f885b242d262', } diff --git a/apps/condo/domains/billing/gql.js b/apps/condo/domains/billing/gql.js index b486d446c1f..3293b2f3a96 100644 --- a/apps/condo/domains/billing/gql.js +++ b/apps/condo/domains/billing/gql.js @@ -34,14 +34,14 @@ const BillingRecipient = generateGqlQueries('BillingRecipient', BILLING_RECIPIEN const BILLING_CATEGORY_FIELDS = `{ name nameNonLocalized ${COMMON_FIELDS} }` const BillingCategory = generateGqlQueries('BillingCategory', BILLING_CATEGORY_FIELDS) -const BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS = 'toPayDetails { charge formula balance recalculation privilege penalty paid }' -const BILLING_RECEIPT_SERVICE_TO_PAY_DETAILS_FIELDS = BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS.replace('}', 'volume tariff measure }') +const BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS = 'charge formula balance recalculation privilege penalty paid' +const BILLING_RECEIPT_SERVICE_TO_PAY_DETAILS_FIELDS = `toPayDetails { ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} volume tariff measure }` const BILLING_RECEIPT_SERVICE_FIELDS = `services { id name toPay ${BILLING_RECEIPT_SERVICE_TO_PAY_DETAILS_FIELDS} }` const BILLING_RECEIPT_RECIPIENT_FIELDS = 'recipient { tin iec bic bankAccount }' -const BILLING_RECEIPT_FIELDS = `{ context ${BILLING_INTEGRATION_ORGANIZATION_CONTEXT_FIELDS} importId property { id, address } account { id, number, unitType, unitName, fullName } period toPay printableNumber ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} ${BILLING_RECEIPT_SERVICE_FIELDS} charge formula balance recalculation privilege penalty paid receiver { id tin iec bic bankAccount isApproved } ${BILLING_RECEIPT_RECIPIENT_FIELDS} ${COMMON_FIELDS} category ${BILLING_CATEGORY_FIELDS} invalidServicesError file { id sensitiveDataFile { id filename originalFilename publicUrl mimetype } publicDataFile { id filename originalFilename publicUrl mimetype } controlSum } }` +const BILLING_RECEIPT_FIELDS = `{ context ${BILLING_INTEGRATION_ORGANIZATION_CONTEXT_FIELDS} importId property { id, address } account { id, number, unitType, unitName, fullName } period toPay printableNumber toPayDetails { ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} } ${BILLING_RECEIPT_SERVICE_FIELDS} charge formula balance recalculation privilege penalty paid receiver { id tin iec bic bankAccount isApproved } ${BILLING_RECEIPT_RECIPIENT_FIELDS} ${COMMON_FIELDS} category ${BILLING_CATEGORY_FIELDS} invalidServicesError file { id sensitiveDataFile { id filename originalFilename publicUrl mimetype } publicDataFile { id filename originalFilename publicUrl mimetype } controlSum } }` const BillingReceipt = generateGqlQueries('BillingReceipt', BILLING_RECEIPT_FIELDS) -const RESIDENT_BILLING_RECEIPTS_FIELDS = `{ id ${BILLING_RECEIPT_RECIPIENT_FIELDS} period toPay paid ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} ${BILLING_RECEIPT_SERVICE_FIELDS} printableNumber serviceConsumer { id paymentCategory } currencyCode category { id name } isPayable file { file { id originalFilename publicUrl mimetype } controlSum } }` +const RESIDENT_BILLING_RECEIPTS_FIELDS = `{ id ${BILLING_RECEIPT_RECIPIENT_FIELDS} period toPay paid toPayDetails { ${BILLING_RECEIPT_TO_PAY_DETAILS_FIELDS} } ${BILLING_RECEIPT_SERVICE_FIELDS} printableNumber serviceConsumer { id paymentCategory } currencyCode category { id name } isPayable file { file { id originalFilename publicUrl mimetype } controlSum } }` const ResidentBillingReceipt = generateGqlQueries('ResidentBillingReceipt', RESIDENT_BILLING_RECEIPTS_FIELDS) const BILLING_RECEIPT_FILE_FIELDS = `{ file { id originalFilename publicUrl mimetype } context { id } receipt { id } controlSum ${COMMON_FIELDS} }` diff --git a/apps/condo/domains/common/components/CountDownTimer.tsx b/apps/condo/domains/common/components/CountDownTimer.tsx index fe396d7c18e..9f28ac1ffb0 100644 --- a/apps/condo/domains/common/components/CountDownTimer.tsx +++ b/apps/condo/domains/common/components/CountDownTimer.tsx @@ -24,6 +24,8 @@ const getCountDownDateFromCookies = (countDownId) => { } if (cookie.match(countDownId)){ + // not a ReDoS issue: running on end user browser + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp const coundownRegexp = new RegExp(`(?:(?:^|.*;\\s*)COUNTDOWN_${countDownId}\\s*=\\s*([^;]*).*$)|^.*$`) const countDownFromCookie = document diff --git a/apps/condo/domains/common/components/MenuItem.tsx b/apps/condo/domains/common/components/MenuItem.tsx index fb19c9509bb..74246f8e00f 100644 --- a/apps/condo/domains/common/components/MenuItem.tsx +++ b/apps/condo/domains/common/components/MenuItem.tsx @@ -12,6 +12,7 @@ import { colors } from '@open-condo/ui/dist/colors' import { Tooltip } from '@condo/domains/common/components/Tooltip' import { transitions } from '@condo/domains/common/constants/style' import { renderLink } from '@condo/domains/common/utils/Renders' +import { getEscaped } from '@condo/domains/common/utils/string.utils' import { INoOrganizationToolTipWrapper } from '@condo/domains/onboarding/hooks/useNoOrganizationToolTip' import { ClientRenderedIcon } from './icons/ClientRenderedIcon' @@ -120,7 +121,9 @@ export const MenuItem: React.FC = (props) => { const [isActive, setIsActive] = useState(false) useDeepCompareEffect(() => { - const escapedPath = path ? path.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : undefined + const escapedPath = path ? getEscaped(path) : undefined + // not a ReDoS issue: running on end user browser + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp const regex = new RegExp(`^${escapedPath}`) setIsActive(path === '/' ? asPath === path diff --git a/apps/condo/domains/common/components/TextHighlighter.tsx b/apps/condo/domains/common/components/TextHighlighter.tsx index f2a05d52601..c5bd1572eee 100644 --- a/apps/condo/domains/common/components/TextHighlighter.tsx +++ b/apps/condo/domains/common/components/TextHighlighter.tsx @@ -30,6 +30,8 @@ export const TextHighlighter: React.FC = (props) => { if (isEmpty(text)) return null let result + // not a ReDoS issue: running on end user browser + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp const searchRegexp = new RegExp(`(${getEscaped(search)})`, 'ig') // NOSONAR if (isEmpty(search) || !searchRegexp.test(text)) { diff --git a/apps/condo/domains/common/components/containers/BehaviorRecorder.tsx b/apps/condo/domains/common/components/containers/BehaviorRecorder.tsx index 52dd667087f..f1785c4feff 100644 --- a/apps/condo/domains/common/components/containers/BehaviorRecorder.tsx +++ b/apps/condo/domains/common/components/containers/BehaviorRecorder.tsx @@ -31,6 +31,9 @@ const BehaviorRecorder = ({ engine }: Props) => { return (
diff --git a/apps/condo/domains/common/components/containers/FormTableBlocks.tsx b/apps/condo/domains/common/components/containers/FormTableBlocks.tsx index 751c2ae5c3b..c0a99dac08e 100644 --- a/apps/condo/domains/common/components/containers/FormTableBlocks.tsx +++ b/apps/condo/domains/common/components/containers/FormTableBlocks.tsx @@ -208,6 +208,9 @@ function toGQLWhere (filters) { Object.keys(filters).forEach((key) => { const v = filters[key] if (v && v.length === 1) { + // where statement going to be sanitized by backend + // and going to use for read requests only + // nosemgrep: javascript.lang.security.insecure-object-assign.insecure-object-assign Object.assign(where, JSON.parse(v[0])) } else if (v && v.length >= 1) { if (where.OR) { diff --git a/apps/condo/domains/common/hooks/useExportTaskUIInterface.tsx b/apps/condo/domains/common/hooks/useExportTaskUIInterface.tsx index 0c412e2f360..0f019b9b86a 100644 --- a/apps/condo/domains/common/hooks/useExportTaskUIInterface.tsx +++ b/apps/condo/domains/common/hooks/useExportTaskUIInterface.tsx @@ -63,6 +63,9 @@ export const useExportTaskUIInterface = ({ if (publicUrl && filename) { await downloadFile({ url: publicUrl, name: filename }) } else { + // this log entry for development & support purposes on end user browser + // no important logs can be hided by injected external console.log formatters + // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring console.error(`File is missing in ${schemaName}`, taskRecord) } }, [downloadFile, schemaName]) diff --git a/apps/condo/domains/common/utils/VersioningMiddleware.js b/apps/condo/domains/common/utils/VersioningMiddleware.js index d52280af4e8..c977c3650b6 100644 --- a/apps/condo/domains/common/utils/VersioningMiddleware.js +++ b/apps/condo/domains/common/utils/VersioningMiddleware.js @@ -4,6 +4,8 @@ const { get } = require('lodash') class VersioningMiddleware { async prepareMiddleware () { + // this route can not be used for csrf attack (because no cookies and tokens are used in a public route) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() app.use('/api/version', (req, res) => { res.status(200).json({ diff --git a/apps/condo/domains/common/utils/createExportFile.js b/apps/condo/domains/common/utils/createExportFile.js index 1e22e0d222c..c28830db80f 100644 --- a/apps/condo/domains/common/utils/createExportFile.js +++ b/apps/condo/domains/common/utils/createExportFile.js @@ -7,8 +7,6 @@ const { v4: uuid } = require('uuid') const FileAdapter = require('./fileAdapter') - - const EXCEL_FILE_META = { mimetype: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', encoding: 'UTF-8', @@ -28,8 +26,11 @@ const render = (pathToTemplate, replaces, options = {}) => new Promise((resolve, // It makes unable to save data to `File` field server-side // @deprecated use `buildExportFile` like in `apps/condo/domains/ticket/tasks/exportTicketsTask.js` async function createExportFile ({ fileName, templatePath, replaces, meta }) { - const ExportFileAdapter = new FileAdapter('export') + // templatePath is a configured template path - not a user input + // all results of export file generation will be accessible only for authorized end users + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const fileContent = await render(path.resolve(templatePath), replaces) + const ExportFileAdapter = new FileAdapter('export') const buffer = new Duplex() buffer.push(fileContent) buffer.push(null) @@ -57,6 +58,8 @@ async function createExportFile ({ fileName, templatePath, replaces, meta }) { * @return {Promise<{ stream }>} */ async function buildExportFile ({ templatePath, replaces, options }) { + // all results of export file generation will be accessible only for authorized end users + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const content = await render(path.resolve(templatePath), replaces, options) const stream = Readable.from(content) return { stream } diff --git a/apps/condo/domains/common/utils/crypto.js b/apps/condo/domains/common/utils/crypto.js index e7f8c740c3f..cf9a5027980 100644 --- a/apps/condo/domains/common/utils/crypto.js +++ b/apps/condo/domains/common/utils/crypto.js @@ -1,10 +1,11 @@ const crypto = require('crypto') /** - * Converts value to MD5 hash + * Converts value to MD5 hash. Do not ise this for hashing sensitive data! * @param value * @returns {string} */ +// nosemgrep: contrib.nodejsscan.crypto_node.node_md5 const md5 = (value) => crypto.createHash('md5').update(value).digest('hex') module.exports = { diff --git a/apps/condo/domains/common/utils/fileAdapter.js b/apps/condo/domains/common/utils/fileAdapter.js index f278634bedc..0cefc2603b0 100644 --- a/apps/condo/domains/common/utils/fileAdapter.js +++ b/apps/condo/domains/common/utils/fileAdapter.js @@ -40,6 +40,9 @@ class LocalFilesMiddleware { } prepareMiddleware () { + // this route serve a static file to the user browser and does not have any operation for csrf attacking + // also, it used for development purposes only (see conf.FILE_FIELD_ADAPTER configuration) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() app.use(this._path, express.static(this._src)) return app diff --git a/apps/condo/domains/common/utils/sberCloudFileAdapter.js b/apps/condo/domains/common/utils/sberCloudFileAdapter.js index 75542384791..50be0865604 100644 --- a/apps/condo/domains/common/utils/sberCloudFileAdapter.js +++ b/apps/condo/domains/common/utils/sberCloudFileAdapter.js @@ -251,6 +251,9 @@ const obsRouterHandler = ({ keystone }) => { class OBSFilesMiddleware { prepareMiddleware ({ keystone }) { + // this route does not have any system change operation and used only for serving files to end user browser + // this mean no csrf attacking possible - since no data change operation going to be made by opening a link + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() app.use('/api/files/:file(*)', obsRouterHandler({ keystone })) return app diff --git a/apps/condo/domains/news/tasks/exportRecipients.js b/apps/condo/domains/news/tasks/exportRecipients.js index 361486c81d8..8eb5ff283e7 100644 --- a/apps/condo/domains/news/tasks/exportRecipients.js +++ b/apps/condo/domains/news/tasks/exportRecipients.js @@ -1,5 +1,3 @@ -const { createHash } = require('crypto') - const dayjs = require('dayjs') const compact = require('lodash/compact') const filter = require('lodash/filter') @@ -18,6 +16,7 @@ const { i18n } = require('@open-condo/locales/loader') const { ERROR, COMPLETED } = require('@condo/domains/common/constants/export') const { TASK_WORKER_FINGERPRINT } = require('@condo/domains/common/constants/tasks') const { buildExportFile: buildExportExcelFile, EXCEL_FILE_META } = require('@condo/domains/common/utils/createExportFile') +const { md5 } = require('@condo/domains/common/utils/crypto') const { getHeadersTranslations, EXPORT_TYPE_NEWS_RECIPIENTS } = require('@condo/domains/common/utils/exportToExcel') const { loadListByChunks } = require('@condo/domains/common/utils/serverSchema') const { buildUploadInputFrom } = require('@condo/domains/common/utils/serverSchema/export') @@ -59,7 +58,7 @@ const buildExportFile = async ({ rows, locale }) => { encoding: EXCEL_FILE_META.encoding, meta: { listkey: 'NewsRecipients', - id: createHash('md5').update(JSON.stringify(rows)).digest('hex'), + id: md5(JSON.stringify(rows)), }, } } diff --git a/apps/condo/domains/notification/adapters/apple/AppleJSONWebToken.spec.js b/apps/condo/domains/notification/adapters/apple/AppleJSONWebToken.spec.js index ddc21897e70..647f31e18a6 100644 --- a/apps/condo/domains/notification/adapters/apple/AppleJSONWebToken.spec.js +++ b/apps/condo/domains/notification/adapters/apple/AppleJSONWebToken.spec.js @@ -8,6 +8,7 @@ const APPLE_CONFIG = { kid: faker.random.alphaNumeric(10), iss: faker.random.alphaNumeric(10), // ES256 key generated by https://8gwifi.org/jwsgen.jsp + // nosemgrep: generic.secrets.gitleaks.private-key.private-key privateKey: '-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIERzC9YnAZv6dTmLPY72gQqLvihwlesD5odf5mx/NNQLoAoGCCqGSM49 AwEHoUQDQgAEvX/e71XLG847HzKpTozogE5pwgaxVN29UkZoNmjP9ZnHcRs7gsBeTGuKwLv0um/C65mb73oy2QeDQCe8R20JAA==\n-----END EC PRIVATE KEY-----', //NOSONAR } diff --git a/apps/condo/domains/notification/adapters/firebaseAdapter.js b/apps/condo/domains/notification/adapters/firebaseAdapter.js index 6d412a013b5..2f15cae1406 100644 --- a/apps/condo/domains/notification/adapters/firebaseAdapter.js +++ b/apps/condo/domains/notification/adapters/firebaseAdapter.js @@ -42,6 +42,8 @@ class FirebaseAdapter { } this.projectId = get(config, 'project_id', null) + // not an user input. No ReDoS regexp expected + // nosemreg: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp this.messageIdPrefixRegexp = new RegExp(`projects/${this.projectId}/messages`) } diff --git a/apps/condo/domains/notification/templates.js b/apps/condo/domains/notification/templates.js index 9a84b2db173..3fca79230d1 100644 --- a/apps/condo/domains/notification/templates.js +++ b/apps/condo/domains/notification/templates.js @@ -25,6 +25,8 @@ const LANG_DIR_RELATED = '../../lang' const TEMPLATE_ENGINE_DEFAULT_DATE_FORMAT = 'D MMMM YYYY' const SERVER_URL = conf.SERVER_URL +// config based path +// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const nunjucks = new Nunjucks.Environment(new Nunjucks.FileSystemLoader(path.resolve(__dirname, LANG_DIR_RELATED))) nunjucks.addFilter('dateFormat', function (dateStr, locale, format) { return dayjs(dateStr).locale(LOCALES[locale || conf.DEFAULT_LOCALE]).format(format || TEMPLATE_ENGINE_DEFAULT_DATE_FORMAT) @@ -38,7 +40,10 @@ nunjucks.addFilter('dateFormat', function (dateStr, locale, format) { * @returns {string} */ function getTemplate (locale, messageType, transportType) { + // this is template reading method and files are distributed as part of source codes + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const defaultTemplatePath = path.resolve(__dirname, `${LANG_DIR_RELATED}/${locale}/messages/${messageType}/${DEFAULT_TEMPLATE_FILE_NAME}`) + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const transportTemplatePath = path.resolve(__dirname, `${LANG_DIR_RELATED}/${locale}/messages/${messageType}/${transportType}.${DEFAULT_TEMPLATE_FILE_EXTENSION}`) if (fs.existsSync(transportTemplatePath)) { @@ -60,8 +65,12 @@ function getTemplate (locale, messageType, transportType) { * @returns {{templatePathText: ?string, templatePathHtml: ?string}} */ function getEmailTemplate (locale, messageType) { + // this is template reading method and files are distributed as part of source codes + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const defaultTemplatePath = path.resolve(__dirname, `${LANG_DIR_RELATED}/${locale}/messages/${messageType}/${DEFAULT_TEMPLATE_FILE_NAME}`) + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const emailTextTemplatePath = path.resolve(__dirname, `${LANG_DIR_RELATED}/${locale}/messages/${messageType}/${EMAIL_TRANSPORT}.${DEFAULT_TEMPLATE_FILE_EXTENSION}`) + // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal const emailHtmlTemplatePath = path.resolve(__dirname, `${LANG_DIR_RELATED}/${locale}/messages/${messageType}/${EMAIL_TRANSPORT}.html.${DEFAULT_TEMPLATE_FILE_EXTENSION}`) let templatePathText = null diff --git a/apps/condo/domains/organization/integrations/sbbol/SbbolRequestApi.js b/apps/condo/domains/organization/integrations/sbbol/SbbolRequestApi.js index caf3a341d54..e679c269e33 100644 --- a/apps/condo/domains/organization/integrations/sbbol/SbbolRequestApi.js +++ b/apps/condo/domains/organization/integrations/sbbol/SbbolRequestApi.js @@ -36,6 +36,8 @@ class SbbolRequestApi { constructor (options) { const { accessToken, host, port, certificate, passphrase } = options const { host: hostname } = new URL(host) + + // nosemgrep: problem-based-packs.insecure-transport.js-node.bypass-tls-verification.bypass-tls-verification this.options = { hostname, port, diff --git a/apps/condo/domains/organization/integrations/sbbol/sync/MockSbbolResponses.js b/apps/condo/domains/organization/integrations/sbbol/sync/MockSbbolResponses.js index 1a53993ba29..244fcfa8af5 100644 --- a/apps/condo/domains/organization/integrations/sbbol/sync/MockSbbolResponses.js +++ b/apps/condo/domains/organization/integrations/sbbol/sync/MockSbbolResponses.js @@ -162,12 +162,15 @@ const EXAMPLE_GET_CLIENT_INFO = { ], } +// just a mocked fake data for test purposes const EXAMPLE_TOKEN_SET = { scope: 'openid PAYROLL name', + // nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key access_token: 'c76fb018-27c9-43f7-a751-62646eda7e1a-1', token_type: 'Bearer', expires_in: 3600, refresh_token: '03e0be32-e72e-47ec-b740-a00b333a8ac4-1', + // nosemgrep: generic.secrets.security.detected-jwt-token.detected-jwt-token id_token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJnb3N0MzQuMTAtMjAxMiJ9.eyJzdWIiOiI2ODM4ZjM1MmI0YzQ0YjZjOGFmYTY0ZTFlZDJmNjg1NzM0MjE4NDAwNjZiZTU3MTgxZjNiN2IyYjc1NThkYmJlIiwiYXVkIjoiMTAwMTMiLCJhY3IiOiJsb2EtMyIsImF6cCI6IjEwMDEzIiwiYXV0aF90aW1lIjoxNTgyMzcwNDk5LCJhbXIiOiJ7cHdkLCBtY2EsIG1mYSwgb3RwLCBzbXN9IiwiaXNzIjoiaHR0cDovL3NidC1vYWZzLTYzODo5MDgwL2ljZGsiLCJleHAiOjE1ODIzNzA4MDEsImlhdCI6MTU4MjM3MDUwMSwibm9uY2UiOiI3YmU2NmFjOS1kMDdjLTQ5NjctYWRlZC1jYTI3MGEyN2U5ZTgiLCJ1c2wiOiJQYXJ0bmVyMzMyMiJ9.IWCyZzOk5nT0GWfhi9n3Nqy8Ii8mJ1eeFS7YRoE-l74lqo6BLksCuaVXt2ErMZYmDyyZscu7ISm0n-YsSrgZPQ', } diff --git a/apps/condo/domains/resident/tasks/helpers/messageTools.js b/apps/condo/domains/resident/tasks/helpers/messageTools.js index 71a60371dc9..6808f2768b3 100644 --- a/apps/condo/domains/resident/tasks/helpers/messageTools.js +++ b/apps/condo/domains/resident/tasks/helpers/messageTools.js @@ -1,4 +1,5 @@ const { get, isEmpty, escapeRegExp } = require('lodash') +const { checkSync } = require('recheck') const { MESSAGE_META } = require('@condo/domains/notification/constants/constants') const { WRONG_MESSAGE_TYPE_PROVIDED_ERROR } = require('@condo/domains/notification/constants/errors') @@ -40,9 +41,16 @@ const renderTemplateString = (templateString, data) => { let result = `${templateString}` for (const key of keys) { - const keyRegexp = new RegExp(`{${escapeRegExp(key)}}`, 'gmi') - - result = result.replace(keyRegexp, data[key] || '') + const flags = 'gmi' + const reDoSCheck = checkSync(key, flags) + + if (reDoSCheck.status !== 'vulnerable') { + const pattern = `{${escapeRegExp(key)}}` + // ReDos injection checked in code above + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp + const keyRegexp = new RegExp(pattern, flags) + result = result.replace(keyRegexp, data[key] || '') + } } return result diff --git a/apps/condo/domains/resident/tasks/helpers/messageTools.spec.js b/apps/condo/domains/resident/tasks/helpers/messageTools.spec.js index 8888065e2fa..1ca6107e774 100644 --- a/apps/condo/domains/resident/tasks/helpers/messageTools.spec.js +++ b/apps/condo/domains/resident/tasks/helpers/messageTools.spec.js @@ -43,6 +43,20 @@ describe('messageTools', () => { expect(result).toEqual([data.a, '{d}', data.c].join(':')) }) + + it('skips template parts having ReDoS pattern key', async () => { + const template = '^a+a+$' // template with ReDoS exploit + // data with ReDoS exploit + const data = { + '^a+a+$': faker.random.alphaNumeric(8), + } + const result = renderTemplateString(template, data) + + // no data should be populated + // since reg exp wasn't run by function - cause data key has ReDoS exploit + // expected original template + expect(result).toEqual(template) + }) }) describe('hydrateItems', () => { diff --git a/apps/condo/domains/ticket/schema/Ticket.js b/apps/condo/domains/ticket/schema/Ticket.js index c06963ec890..fb38ad398b7 100644 --- a/apps/condo/domains/ticket/schema/Ticket.js +++ b/apps/condo/domains/ticket/schema/Ticket.js @@ -1,9 +1,6 @@ /** * Generated by `createschema ticket.Ticket organization:Text; statusReopenedCounter:Integer; statusReason?:Text; status:Relationship:TicketStatus:PROTECT; number?:Integer; client?:Relationship:User:SET_NULL; clientName:Text; clientEmail:Text; clientPhone:Text; operator:Relationship:User:SET_NULL; assignee?:Relationship:User:SET_NULL; details:Text; meta?:Json;` */ - -const crypto = require('crypto') - const { Text, Relationship, Integer, DateTimeUtc, Checkbox, Select } = require('@keystonejs/fields') const dayjs = require('dayjs') const { isEmpty, get, isNull, compact, isArray, isString, uniq } = require('lodash') @@ -30,6 +27,7 @@ const { ADDRESS_META_FIELD, UNIT_TYPE_FIELD, } = require('@condo/domains/common/schema/fields') +const { md5 } = require('@condo/domains/common/utils/crypto') const { buildSetOfFieldsToTrackFrom, storeChangesIfUpdated } = require('@condo/domains/common/utils/serverSchema/changeTrackable') const { normalizeText } = require('@condo/domains/common/utils/text') const { hasDbFields } = require('@condo/domains/common/utils/validation.utils') @@ -212,7 +210,7 @@ const checkDailyTicketLimit = async ({ userId, organizationId, details, context } const byIdAndOrgKey = `dailyTicketLimit:id:${userId}:organization:${organizationId}` - const byIdOrgAndDetailsKey = `${byIdAndOrgKey}:details:${crypto.createHash('md5').update(details).digest('hex')}` + const byIdOrgAndDetailsKey = `${byIdAndOrgKey}:details:${md5(details)}` const byIdOrgAndDetailsCounter = await redisGuard.incrementDayCounter(byIdOrgAndDetailsKey) if (byIdOrgAndDetailsCounter > DAILY_SAME_TICKET_LIMIT) { diff --git a/apps/condo/domains/ticket/utils/serverSchema/TicketChange.js b/apps/condo/domains/ticket/utils/serverSchema/TicketChange.js index b6e8231dada..ee15e44570d 100644 --- a/apps/condo/domains/ticket/utils/serverSchema/TicketChange.js +++ b/apps/condo/domains/ticket/utils/serverSchema/TicketChange.js @@ -143,6 +143,10 @@ const resolveManyToManyField = async (fieldName, ref, displayNameAttr = 'name', variables: { id: existingItem.id }, }) if (updatedResult.errors) { + // this log entry for development & support purposes only + // no important logs can be hided by injected external console.log formatters + // no logs formatters can be injected + // nosemgrep: javascript.lang.security.audit.unsafe-formatstring.unsafe-formatstring console.error(`Error while fetching related ${fieldName} in manyToManyResolver of changeTrackable for a Ticket`, updatedResult.errors) return {} } diff --git a/apps/condo/domains/user/integration/UserExternalIdentityMiddleware.js b/apps/condo/domains/user/integration/UserExternalIdentityMiddleware.js index d53d5fa1635..e674c9eb6c9 100644 --- a/apps/condo/domains/user/integration/UserExternalIdentityMiddleware.js +++ b/apps/condo/domains/user/integration/UserExternalIdentityMiddleware.js @@ -9,6 +9,9 @@ const { SberIdRoutes } = require('@condo/domains/user/integration/sberid/routes' class UserExternalIdentityMiddleware { async prepareMiddleware ({ keystone }) { + // all bellow routes are handling csrf properly + // and controlling start/end authorization sources (browsers, mobile clients, etc) + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() // sbbol route diff --git a/apps/condo/domains/user/integration/appleid/utils/validations.spec.js b/apps/condo/domains/user/integration/appleid/utils/validations.spec.js index 3a37726c4bf..3ee2df07a9b 100644 --- a/apps/condo/domains/user/integration/appleid/utils/validations.spec.js +++ b/apps/condo/domains/user/integration/appleid/utils/validations.spec.js @@ -1,3 +1,4 @@ +const { faker } = require('@faker-js/faker') const jwt = require('jsonwebtoken') const { APPLE_ID_SESSION_KEY } = require('@condo/domains/user/constants/common') @@ -7,6 +8,8 @@ const { validateNonce, } = require('./validations') +const JWT_SECRET = faker.datatype.uuid() + const getReqWithSessionParam = (path, value) => { return { session: { @@ -19,7 +22,7 @@ const getReqWithSessionParam = (path, value) => { } const getTokenSet = (nonce) => { return { - idToken: jwt.sign({ nonce }, 'testKey'), + idToken: jwt.sign({ nonce }, JWT_SECRET), } } diff --git a/apps/condo/domains/user/integration/sberid/utils/validations.spec.js b/apps/condo/domains/user/integration/sberid/utils/validations.spec.js index 118cca4c074..dcd35c1ef06 100644 --- a/apps/condo/domains/user/integration/sberid/utils/validations.spec.js +++ b/apps/condo/domains/user/integration/sberid/utils/validations.spec.js @@ -1,3 +1,4 @@ +const { faker } = require('@faker-js/faker') const jwt = require('jsonwebtoken') const { SBER_ID_SESSION_KEY } = require('@condo/domains/user/constants/common') @@ -8,6 +9,8 @@ const { hasSamePhone, } = require('./validations') +const JWT_SECRET = faker.datatype.uuid() + const getReqWithSessionParam = (path, value) => { return { session: { @@ -20,7 +23,7 @@ const getReqWithSessionParam = (path, value) => { } const getTokenSet = (nonce) => { return { - idToken: jwt.sign({ nonce }, 'testKey'), + idToken: jwt.sign({ nonce }, JWT_SECRET), } } diff --git a/apps/condo/domains/user/oidc/OIDCMiddleware.js b/apps/condo/domains/user/oidc/OIDCMiddleware.js index 1ccf455c5cd..536fa198ef8 100644 --- a/apps/condo/domains/user/oidc/OIDCMiddleware.js +++ b/apps/condo/domains/user/oidc/OIDCMiddleware.js @@ -19,6 +19,10 @@ class OIDCMiddleware { // const Provider = require('oidc-provider') const provider = new Provider(conf.SERVER_URL, createConfiguration(keystone, conf)) + + // all bellow routes are handling csrf properly using oidc-client + // also, all operations in those routes just adding grands for end users - not a csrf attack source + // nosemgrep: javascript.express.security.audit.express-check-csurf-middleware-usage.express-check-csurf-middleware-usage const app = express() OIDCBearerTokenKeystonePatch(app, keystone) diff --git a/apps/condo/domains/user/utils/serverSchema/index.js b/apps/condo/domains/user/utils/serverSchema/index.js index 133ef8ddb7a..d4fb3e52e34 100644 --- a/apps/condo/domains/user/utils/serverSchema/index.js +++ b/apps/condo/domains/user/utils/serverSchema/index.js @@ -22,6 +22,7 @@ const { SIGNIN_AS_USER_MUTATION } = require('@condo/domains/user/gql') const { REGISTER_NEW_SERVICE_USER_MUTATION } = require('@condo/domains/user/gql') const { SEND_MESSAGE_TO_SUPPORT_MUTATION } = require('@condo/domains/user/gql') const { RESET_USER_MUTATION } = require('@condo/domains/user/gql') +// nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key const { OidcClient: OidcClientGQL } = require('@condo/domains/user/gql') const { ExternalTokenAccessRight: ExternalTokenAccessRightGQL } = require('@condo/domains/user/gql') const { GET_ACCESS_TOKEN_BY_USER_ID_QUERY } = require('@condo/domains/user/gql') diff --git a/apps/condo/initialData.js b/apps/condo/initialData.js index a3fe6bdaf71..1f496ce9fe5 100644 --- a/apps/condo/initialData.js +++ b/apps/condo/initialData.js @@ -9,6 +9,8 @@ module.exports = [ name: 'Admin', email: 'admin@example.com', phone: '+79068888888', + // this is development only data + // nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key password: '3a74b3f07978', isAdmin: true, }, @@ -20,6 +22,8 @@ module.exports = [ name: 'JustUser', email: 'user@example.com', phone: '+79067777777', + // this is development only data + // nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key password: '1a92b3a07c78', isAdmin: false, }, diff --git a/apps/condo/package.json b/apps/condo/package.json index 1f2fbc85a3d..93f01237aba 100644 --- a/apps/condo/package.json +++ b/apps/condo/package.json @@ -152,6 +152,7 @@ "react-progressive-image": "^0.6.0", "react-test-renderer": "^16.13.1", "react-yandex-metrika": "^2.6.0", + "recheck": "^4.4.5", "redlock": "^5.0.0-beta.2", "serialize-error": "^8.1.0", "swr": "^1.3.0", diff --git a/apps/condo/pages/_app.tsx b/apps/condo/pages/_app.tsx index 8f8476b88b8..181330cd780 100644 --- a/apps/condo/pages/_app.tsx +++ b/apps/condo/pages/_app.tsx @@ -258,6 +258,8 @@ const MenuItems: React.FC = () => { icon: AllIcons['Services'], label: 'global.section.miniapps', access: !isAssignedVisibilityType && isManagingCompany, + // not a ReDoS issue: running on end user browser + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp excludePaths: connectedAppsIds.map((id) => new RegExp(`/miniapps/${id}$`)), }, ].filter(checkItemAccess), @@ -318,8 +320,11 @@ const MenuItems: React.FC = () => { excludePaths={item.excludePaths} /> ))} - {get(appsByCategories, category.key, []).map((app) => ( - { + // not a ReDoS issue: running on end user browser + // nosemgrep: javascript.lang.security.audit.detect-non-literal-regexp.detect-non-literal-regexp + const miniAppsPattern = new RegExp(`/miniapps/${app.id}/.+`) + return { disabled={disabled} isCollapsed={isCollapsed} toolTipDecorator={disabled ? wrapElementIntoNoOrganizationToolTip : null} - excludePaths={[new RegExp(`/miniapps/${app.id}/.+`)]} + excludePaths={[miniAppsPattern]} /> - ))} + })} ))} diff --git a/apps/condo/pages/ticket/index.tsx b/apps/condo/pages/ticket/index.tsx index c4927355014..2ad8ea6f048 100644 --- a/apps/condo/pages/ticket/index.tsx +++ b/apps/condo/pages/ticket/index.tsx @@ -322,6 +322,7 @@ const TicketTable = ({ /> ), selectedTicketKeys.length < 1 && TicketImportButton && TicketImportButton, + // nosemgrep: generic.secrets.gitleaks.generic-api-key.generic-api-key selectedTicketKeys.length < 1 && , selectedTicketKeys.length > 0 && (