VYPR
Low severityGHSA Advisory· Published Jun 5, 2026· Updated Jun 5, 2026

NocoDB: User Enumeration via Sign-In Timing

CVE-2026-47380

Description

Summary

Sign-in response timing differed between known and unknown email addresses because the unknown-user branch returned without performing a password hash comparison.

Details

The unknown-user branch in auth.service.ts now performs a bcrypt.compare against a fixed dummy hash so the response time of failed sign-ins is approximately independent of whether the address exists. Rate limiting on the sign-in endpoint is implemented in the Enterprise build only and is not affected by this advisory.

Impact

A network-positioned attacker could enumerate registered email addresses by timing sign-in responses. Exploitation requires only the ability to send unauthenticated sign-in requests.

Credit

This issue was reported by @AndyAnh174.

Affected products

1

Patches

1
4e6037f9fa1e

Merge pull request #13527 from nocodb/nc-feat/improve-feat-gating

https://github.com/nocodb/nocodbRamesh ManeApr 15, 2026Fixed in 2026.04.1via release-tag
6 files changed · +96 15
  • packages/nc-gui/components/dashboard/TreeView/Table/Node.vue+17 9 modified
    @@ -675,17 +675,25 @@ const enabledOptions = computed(() => {
                           </NcMenuItem>
                         </template>
                       </PaymentUpgradeBadgeProvider>
    -                  <NcMenuItem
    +                  <PaymentUpgradeBadgeProvider
                         v-if="enabledOptions.tableRowLevelSecurity"
    -                    :data-testid="`sidebar-table-rls-${table.title}`"
    -                    class="nc-table-rls"
    -                    @click="onRowLevelSecurity"
    +                    :feature="PlanFeatureTypes.FEATURE_RLS"
                       >
    -                    <div v-e="['c:table:rls']" class="flex gap-2 items-center w-full">
    -                      <GeneralIcon icon="ncShield" class="opacity-80" />
    -                      <div class="flex-1">{{ $t('objects.permissions.rlsPolicy.rowLevelSecurity') }}</div>
    -                    </div>
    -                  </NcMenuItem>
    +                    <template #default="{ click }">
    +                      <NcMenuItem
    +                        :data-testid="`sidebar-table-rls-${table.title}`"
    +                        class="nc-table-rls"
    +                        @click="click(PlanFeatureTypes.FEATURE_RLS, onRowLevelSecurity)"
    +                      >
    +                        <div v-e="['c:table:rls']" class="flex gap-2 items-center w-full">
    +                          <GeneralIcon icon="ncShield" class="opacity-80" />
    +                          <div class="flex-1">{{ $t('objects.permissions.rlsPolicy.rowLevelSecurity') }}</div>
    +
    +                          <LazyPaymentUpgradeBadge :feature="PlanFeatureTypes.FEATURE_RLS" remove-click />
    +                        </div>
    +                      </NcMenuItem>
    +                    </template>
    +                  </PaymentUpgradeBadgeProvider>
                       <PaymentUpgradeBadgeProvider
                         v-if="enabledOptions.tableDateDependency"
                         :feature="PlanFeatureTypes.FEATURE_DATE_DEPENDENCY"
    
  • packages/nc-gui/components/workspace/integrations/IntegrationsTab.vue+14 3 modified
    @@ -1,6 +1,6 @@
     <script lang="ts" setup>
     import type { VNodeRef } from '@vue/runtime-core'
    -import { IntegrationCategoryType } from 'nocodb-sdk'
    +import { IntegrationCategoryType, PlanFeatureTypes } from 'nocodb-sdk'
     import NcModal from '~/components/nc/Modal.vue'
     
     import { type IntegrationItemType, SyncDataType } from '#imports'
    @@ -42,7 +42,7 @@ const { activeWorkspace } = storeToRefs(useWorkspace())
     
     const { isSyncFeatureEnabled } = storeToRefs(useSyncStore())
     
    -const { isEEFeatureBlocked } = useEeConfig()
    +const { isEEFeatureBlocked, blockAiIntegrations, showUpgradeToUseAiIntegrations } = useEeConfig()
     
     const easterEggToggle = computed(() => isFeatureEnabled(FEATURE_FLAG.INTEGRATIONS))
     
    @@ -238,6 +238,11 @@ const handleAddIntegration = async (category: IntegrationCategoryType, integrati
         return
       }
     
    +  if (category === IntegrationCategoryType.AI && blockAiIntegrations.value) {
    +    showUpgradeToUseAiIntegrations({})
    +    return
    +  }
    +
       await addIntegration(integration)
     }
     
    @@ -464,8 +469,14 @@ watch(activeViewTab, (value) => {
                         >
                           <div class="category-type-title flex gap-2">
                             {{ $t(category.title) }}
    +                        <LazyPaymentUpgradeBadge
    +                          v-if="category.value === IntegrationCategoryType.AI && blockAiIntegrations"
    +                          :feature="PlanFeatureTypes.FEATURE_AI_INTEGRATIONS"
    +                          :feature-enabled-callback="() => !blockAiIntegrations"
    +                          remove-click
    +                        />
                             <NcBadge
    -                          v-if="!category.isAvailable"
    +                          v-else-if="!category.isAvailable"
                               :border="false"
                               class="text-nc-content-brand !h-5 bg-nc-bg-brand text-xs font-normal px-2"
                               >{{ $t('msg.toast.futureRelease') }}</NcBadge
    
  • packages/nc-gui/components/workspace/ViewTabs.vue+14 3 modified
    @@ -13,7 +13,7 @@ const { loadCollaborators } = workspaceStore
     
     const { isUIAllowed, isBaseRolesLoaded } = useRoles()
     
    -const { isWsAuditEnabled, handleUpgradePlan, showUpgradeToUseTeams } = useEeConfig()
    +const { isWsAuditEnabled, handleUpgradePlan, showUpgradeToUseTeams, blockTeamsManagement, isEEFeatureBlocked } = useEeConfig()
     
     const { hasTeamsEditPermission, wsTabVisibility } = useWorkspaceTabVisibility(activeWorkspace)
     
    @@ -22,7 +22,7 @@ interface TabItem {
       key: string
       icon: string
       label: string
    -  upgradeBadge?: { feature: string }
    +  upgradeBadge?: { feature: PlanFeatureTypes; blocked: boolean }
       hidden?: boolean
     }
     
    @@ -38,6 +38,9 @@ const tabItems = computed<TabItem[]>(() => {
           key: 'teams',
           icon: 'ncBuilding',
           label: t('general.teams'),
    +      upgradeBadge: isEEFeatureBlocked.value
    +        ? { feature: PlanFeatureTypes.FEATURE_TEAM_MANAGEMENT, blocked: blockTeamsManagement.value }
    +        : undefined, // Only show badge in On-prem without license
           hidden: !wsTabVisibility.value.teams,
         },
         {
    @@ -56,6 +59,9 @@ const tabItems = computed<TabItem[]>(() => {
           key: 'audits',
           icon: 'audit',
           label: t('title.audits'),
    +      upgradeBadge: isEEFeatureBlocked.value
    +        ? { feature: PlanFeatureTypes.FEATURE_AUDIT_WORKSPACE, blocked: !isWsAuditEnabled.value }
    +        : undefined, // Only show badge in On-prem without license
           hidden: !wsTabVisibility.value.audits,
         },
         {
    @@ -105,7 +111,12 @@ const activeTab = computed({
             <div class="tab-title">
               <GeneralIcon :icon="item.icon" class="h-4 w-4" />
               {{ item.label }}
    -          <LazyPaymentUpgradeBadge v-if="item.upgradeBadge" :feature="item.upgradeBadge.feature" remove-click />
    +          <LazyPaymentUpgradeBadge
    +            v-if="item.upgradeBadge"
    +            :feature="item.upgradeBadge.feature"
    +            :feature-enabled-callback="() => !item.upgradeBadge!.blocked"
    +            remove-click
    +          />
             </div>
           </template>
         </a-tab-pane>
    
  • packages/nc-gui/composables/useEeConfig.ts+45 0 modified
    @@ -90,6 +90,8 @@ export const useEeConfig = createSharedComposable(() => {
     
       const blockAiChat = computed(() => true)
     
    +  const blockAiIntegrations = computed(() => true)
    +
       const blockDocAi = computed(() => true)
     
       const blockButtonVisibility = computed(() => true)
    @@ -212,6 +214,8 @@ export const useEeConfig = createSharedComposable(() => {
     
       const showUpgradeToUseAiChat = (..._args: any[]) => {}
     
    +  const showUpgradeToUseAiIntegrations = (..._args: any[]) => {}
    +
       const showUpgradeToUseDocAi = (..._args: any[]) => {}
     
       const showUpgradeToUseButtonVisibility = (..._args: any[]) => {}
    @@ -234,8 +238,16 @@ export const useEeConfig = createSharedComposable(() => {
     
       const blockRecordTemplates = computed(() => false)
     
    +  const blockRls = computed(() => true)
    +
       const showUpgradeToUseRecordTemplates = (..._args: any[]) => {}
     
    +  const showUpgradeToUseRls = (..._args: any[]) => {}
    +
    +  const showUpgradeToDuplicateTableToOtherWs = (..._args: any[]) => {}
    +
    +  const showUpgradeToDuplicateTableToOtherBase = (..._args: any[]) => {}
    +
       const blockFormScheduling = computed(() => true)
     
       const showUpgradeToUseFormScheduling = (..._args: any[]) => {}
    @@ -269,12 +281,30 @@ export const useEeConfig = createSharedComposable(() => {
     
       const showUpgradeForEEFeature = (..._args: any[]) => {}
     
    +  const blockSSO = computed(() => true)
    +
       const showUpgradeToUseSSO = (..._args: any[]) => {}
     
       const blockScim = computed(() => true)
     
       const showUpgradeToUseScim = (..._args: any[]) => {}
     
    +  const blockSnapshots = computed(() => true)
    +
    +  const showUpgradeToUseSnapshots = (..._args: any[]) => {}
    +
    +  const blockCustomUrls = computed(() => true)
    +
    +  const showUpgradeToUseCustomUrls = (..._args: any[]) => {}
    +
    +  const blockScripts = computed(() => true)
    +
    +  const showUpgradeToUseScripts = (..._args: any[]) => {}
    +
    +  const blockWorkflows = computed(() => true)
    +
    +  const showUpgradeToUseWorkflows = (..._args: any[]) => {}
    +
       return {
         calculatePrice,
         getLimit,
    @@ -358,6 +388,8 @@ export const useEeConfig = createSharedComposable(() => {
         showUpgradeToUseAiButtonField,
         blockAiChat,
         showUpgradeToUseAiChat,
    +    blockAiIntegrations,
    +    showUpgradeToUseAiIntegrations,
         blockDocAi,
         showUpgradeToUseDocAi,
         blockButtonVisibility,
    @@ -373,13 +405,17 @@ export const useEeConfig = createSharedComposable(() => {
         isHigherActivePlan,
         blockCardFieldHeaderVisibility,
         blockSync,
    +    blockRls,
         blockUnique,
         blockUuidField,
         blockAutoNumberField,
         showUpgradeToUseSync,
    +    showUpgradeToUseRls,
         showUpgradeToUseUnique,
         showUpgradeToUseUuidField,
         showUpgradeToUseAutoNumberField,
    +    showUpgradeToDuplicateTableToOtherWs,
    +    showUpgradeToDuplicateTableToOtherBase,
         blockAddNewSandbox,
         showSandboxPlanLimitExceededModal,
         blockRecordTemplates,
    @@ -403,8 +439,17 @@ export const useEeConfig = createSharedComposable(() => {
         showUpgradeToCreateWorkspace,
         showUpgradeToManageWorkspaceMembers,
         showUpgradeForEEFeature,
    +    blockSSO,
         showUpgradeToUseSSO,
         blockScim,
         showUpgradeToUseScim,
    +    blockSnapshots,
    +    showUpgradeToUseSnapshots,
    +    blockCustomUrls,
    +    showUpgradeToUseCustomUrls,
    +    blockScripts,
    +    showUpgradeToUseScripts,
    +    blockWorkflows,
    +    showUpgradeToUseWorkflows,
       }
     })
    
  • packages/nc-gui/lang/en.json+2 0 modified
    @@ -209,6 +209,8 @@
         "upgradeToUseAiButtonFieldSubtitle": "Upgrade to the {plan} plan to let AI Button use record data with NocoAI to fill multiple fields automatically.",
         "upgradeToUseAiChat": "Upgrade to use AI Chat",
         "upgradeToUseAiChatSubtitle": "Upgrade to the {plan} plan to use AI Chat to manage your base with natural language.",
    +    "upgradeToUseAiIntegrations": "Upgrade to add AI Integrations",
    +    "upgradeToUseAiIntegrationsSubtitle": "Upgrade to the {plan} plan to connect your own AI provider.",
         "upgradeToUseDocAi": "Upgrade to use Document AI",
         "upgradeToUseDocAiSubtitle": "Upgrade to the {plan} plan to use AI writing, summarization, and translation in documents.",
         "upgradeToUseColourField": "Upgrade to use Colour fields",
    
  • packages/nocodb-sdk/src/lib/payment/index.ts+4 0 modified
    @@ -103,6 +103,8 @@ export enum PlanFeatureTypes {
       FEATURE_DATE_DEPENDENCY = 'feature_date_dependency',
       FEATURE_API_COMMENT_V3 = 'feature_api_comment_v3',
       FEATURE_API_WORKFLOW_MANAGEMENT = 'feature_api_workflow_management',
    +  /** On-prem: core EE capability flag — true for all paid plans, false for free */
    +  FEATURE_EE_CORE = 'feature_ee_core',
     }
     
     export enum PlanTitles {
    @@ -113,6 +115,7 @@ export enum PlanTitles {
     }
     
     export enum OnPremPlanTitles {
    +  FREE = 'Free',
       SELF_HOSTED_STARTER = 'Self-hosted Starter',
       SELF_HOSTED_SCALE = 'Self-hosted Scale',
       SELF_HOSTED_ENTERPRISE = 'Self-hosted Enterprise',
    @@ -362,6 +365,7 @@ export const PlanFeatureUpgradeMessages: Record<PlanFeatureTypes, string> = {
       [PlanFeatureTypes.FEATURE_DATE_DEPENDENCY]: 'to use date dependencies.',
       [PlanFeatureTypes.FEATURE_API_COMMENT_V3]: 'to use comment api.',
       [PlanFeatureTypes.FEATURE_API_WORKFLOW_MANAGEMENT]: 'to use workflow api.',
    +  [PlanFeatureTypes.FEATURE_EE_CORE]: 'to access enterprise features.',
     };
     
     export const getUpgradeMessage = (
    

Vulnerability mechanics

No source-code context for this CVE — mechanics is only generated when we can read the actual fix diff. Without that, the four sections (root cause, attack vector, affected code, fix) would be speculation rather than analysis.

References

3

News mentions

1