VYPR
Medium severity5.1NVD Advisory· Published Jun 5, 2026· Updated Jun 5, 2026

NocoDB: Reflected Cross-Site Scripting via Password Reset Token

CVE-2026-47376

Description

NocoDB's password reset page suffers from reflected XSS due to improper handling of URL tokens in EJS templates.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

NocoDB's password reset page suffers from reflected XSS due to improper handling of URL tokens in EJS templates.

Vulnerability

The password-reset page in NocoDB versions prior to 2026.04.1 rendered the URL token directly into a JavaScript string literal within an EJS template. While EJS escapes some characters, it does not escape single quotes or backslashes, allowing a crafted token to break out of the JavaScript string context and execute arbitrary script.

Exploitation

An attacker can craft a malicious password-reset link containing a token with special characters, such as ';alert(document.cookie);//. A victim clicking this link will trigger the reflected XSS vulnerability within the NocoDB origin. No authentication or special privileges are required beyond the victim following the link.

Impact

Successful exploitation allows an attacker to execute arbitrary JavaScript in the context of the victim's NocoDB origin. This can lead to the theft of authentication tokens, session hijacking, and the ability for the attacker to perform actions on behalf of the victim within NocoDB.

Mitigation

NocoDB version 2026.04.1, released on April 1, 2026, addresses this vulnerability by moving the token into an HTML attribute and reading it via dataset.token at runtime, where EJS's HTML-entity escaping is sufficient. Users should update to version 2026.04.1 or later. [3]

AI Insight generated on Jun 5, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
36aab6d936e5

fix: OSS release prep — docs permission, token expiry, view order

https://github.com/nocodb/nocodbPranavApr 13, 2026Fixed in 2026.04.1via ghsa-release-walk
3 files changed · +45 127
  • packages/nc-gui/components/account/TokenCreateWizard.vue+1 82 modified
    @@ -11,40 +11,15 @@ const emit = defineEmits(['created', 'saved', 'cancel'])
     
     const { api } = useApi()
     const { copy } = useCopy()
    -const { t } = useI18n()
     const { $e } = useNuxtApp()
     
     const isCreating = ref(false)
     const showResultModal = ref(false)
     const createdTokenValue = ref('')
     
     const tokenName = ref('')
    -const expiryOption = ref('90d')
    -const customExpiry = ref('')
    -
    -const showExpiryDropdown = ref(false)
     const tokenCopied = ref(false)
     
    -const formatDate = (days: number) => {
    -  const date = new Date()
    -  date.setDate(date.getDate() + days)
    -  return date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
    -}
    -
    -const expiryOptions = computed(() => [
    -  { value: '7d', label: `7 days (${formatDate(7)})` },
    -  { value: '30d', label: `30 days (${formatDate(30)})` },
    -  { value: '60d', label: `60 days (${formatDate(60)})` },
    -  { value: '90d', label: `90 days (${formatDate(90)})` },
    -  { value: '1y', label: `1 year (${formatDate(365)})` },
    -  { value: 'custom', label: t('labels.custom') },
    -  { value: 'none', label: t('labels.noExpiration') },
    -])
    -
    -const selectedExpiryLabel = computed(() => {
    -  return expiryOptions.value.find((o) => o.value === expiryOption.value)?.label || expiryOption.value
    -})
    -
     const isFormValid = computed(() => {
       return tokenName.value.length > 0 && tokenName.value.length <= 255
     })
    @@ -96,43 +71,6 @@ const onResultDone = () => {
             <a-input v-model:value="tokenName" class="!rounded-lg max-w-150" :maxlength="255" data-testid="nc-token-name-input" />
           </div>
     
    -      <!-- Expiration -->
    -      <div class="flex flex-col gap-2">
    -        <label class="text-sm font-bold text-nc-content-gray">{{ $t('labels.expiration') }}</label>
    -        <div class="flex items-center gap-2">
    -          <NcDropdown v-model:visible="showExpiryDropdown" :trigger="['click']" placement="bottomLeft">
    -            <button class="nc-expiry-pill" data-testid="nc-token-expiry-select">
    -              <span class="text-xs font-semibold text-nc-content-gray-extreme">{{ selectedExpiryLabel }}</span>
    -              <GeneralIcon icon="arrowDown" class="w-3 h-3 text-nc-content-gray-muted ml-auto" />
    -            </button>
    -
    -            <template #overlay>
    -              <NcMenu variant="small" class="!min-w-52">
    -                <NcMenuItem
    -                  v-for="opt in expiryOptions"
    -                  :key="opt.value"
    -                  :class="{ '!bg-nc-bg-gray-light': expiryOption === opt.value }"
    -                  @click="
    -                    () => {
    -                      expiryOption = opt.value
    -                      showExpiryDropdown = false
    -                    }
    -                  "
    -                >
    -                  {{ opt.label }}
    -                </NcMenuItem>
    -              </NcMenu>
    -            </template>
    -          </NcDropdown>
    -          <a-date-picker
    -            v-if="expiryOption === 'custom'"
    -            v-model:value="customExpiry"
    -            class="nc-expiry-datepicker flex-1 max-w-40"
    -            :disabled-date="(d: any) => d && d < new Date()"
    -          />
    -        </div>
    -      </div>
    -
           <!-- Actions -->
           <div class="flex justify-end gap-3 pt-4 border-t border-nc-border-gray-light">
             <NcButton type="text" size="small" data-testid="nc-token-cancel-btn" @click="emit('cancel')">
    @@ -201,23 +139,4 @@ const onResultDone = () => {
       </div>
     </template>
     
    -<style lang="scss" scoped>
    -.nc-expiry-pill {
    -  @apply flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg
    -    bg-nc-bg-gray-light border-1 border-nc-border-gray-medium
    -    cursor-pointer transition-all w-56;
    -
    -  &:hover {
    -    @apply bg-nc-bg-gray-medium;
    -  }
    -}
    -
    -.nc-expiry-datepicker {
    -  @apply !rounded-lg !border-nc-border-gray-medium !shadow-none;
    -
    -  &:deep(.ant-picker-focused),
    -  &.ant-picker-focused {
    -    @apply !border-nc-border-gray-medium !shadow-none;
    -  }
    -}
    -</style>
    +<style lang="scss" scoped></style>
    
  • packages/nc-gui/components/dashboard/TreeView/CreateViewBtn.vue+34 34 modified
    @@ -198,6 +198,40 @@ async function onOpenModal({
                 <GeneralLoader v-if="toBeCreateType === ViewTypes.CALENDAR && isViewListLoading" />
               </div>
             </NcMenuItem>
    +        <NcMenuItem
    +          v-if="isEeUI && showEEFeatures"
    +          inner-class="w-full"
    +          data-testid="sidebar-view-create-map"
    +          @click="
    +            () => {
    +              isOpen = false
    +              showUpgradeToUseMapView({
    +                successCallback: () => {
    +                  onOpenModal({ type: ViewTypes.MAP })
    +                },
    +              })
    +            }
    +          "
    +        >
    +          <div class="item">
    +            <div class="item-inner">
    +              <GeneralViewIcon :meta="{ type: ViewTypes.MAP }" />
    +              <div>{{ $t('objects.viewType.map') }}</div>
    +            </div>
    +
    +            <template v-if="blockMapView">
    +              <PaymentUpgradeBadge
    +                :feature="PlanFeatureTypes.FEATURE_MAP_VIEW"
    +                :plan-title="PlanTitles.BUSINESS"
    +                remove-click
    +                show-as-lock
    +              />
    +            </template>
    +            <template v-else>
    +              <GeneralLoader v-if="toBeCreateType === ViewTypes.MAP && isViewListLoading" />
    +            </template>
    +          </div>
    +        </NcMenuItem>
             <NcTooltip
               v-if="isListViewEnabled"
               :title="$t('tooltip.listViewOnlyPg')"
    @@ -238,40 +272,6 @@ async function onOpenModal({
                 </div>
               </NcMenuItem>
             </NcTooltip>
    -        <NcMenuItem
    -          v-if="isEeUI && showEEFeatures"
    -          inner-class="w-full"
    -          data-testid="sidebar-view-create-map"
    -          @click="
    -            () => {
    -              isOpen = false
    -              showUpgradeToUseMapView({
    -                successCallback: () => {
    -                  onOpenModal({ type: ViewTypes.MAP })
    -                },
    -              })
    -            }
    -          "
    -        >
    -          <div class="item">
    -            <div class="item-inner">
    -              <GeneralViewIcon :meta="{ type: ViewTypes.MAP }" />
    -              <div>{{ $t('objects.viewType.map') }}</div>
    -            </div>
    -
    -            <template v-if="blockMapView">
    -              <PaymentUpgradeBadge
    -                :feature="PlanFeatureTypes.FEATURE_MAP_VIEW"
    -                :plan-title="PlanTitles.BUSINESS"
    -                remove-click
    -                show-as-lock
    -              />
    -            </template>
    -            <template v-else>
    -              <GeneralLoader v-if="toBeCreateType === ViewTypes.MAP && isViewListLoading" />
    -            </template>
    -          </div>
    -        </NcMenuItem>
             <NcMenuItem
               v-if="isEeUI && showEEFeatures"
               inner-class="w-full"
    
  • packages/nc-gui/components/smartsheet/topbar/ViewListDropdown.vue+10 11 modified
    @@ -240,6 +240,16 @@ async function onOpenModal({
                         {{ $t('objects.viewType.calendar') }}
                       </div>
                     </a-menu-item>
    +                <a-menu-item
    +                  v-if="isEeUI && showEEFeatures"
    +                  data-testid="topbar-view-create-map"
    +                  @click="showUpgradeToUseMapView({ successCallback: () => onOpenModal({ type: ViewTypes.MAP }) })"
    +                >
    +                  <div class="nc-viewlist-submenu-popup-item">
    +                    <GeneralViewIcon :meta="{ type: ViewTypes.MAP }" />
    +                    {{ $t('objects.viewType.map') }}
    +                  </div>
    +                </a-menu-item>
                     <NcTooltip
                       v-if="isListViewEnabled"
                       :title="$t('tooltip.listViewOnlyPg')"
    @@ -262,17 +272,6 @@ async function onOpenModal({
                         </div>
                       </a-menu-item>
                     </NcTooltip>
    -                <a-menu-item
    -                  v-if="isEeUI && showEEFeatures"
    -                  data-testid="topbar-view-create-map"
    -                  @click="showUpgradeToUseMapView({ successCallback: () => onOpenModal({ type: ViewTypes.MAP }) })"
    -                >
    -                  <div class="nc-viewlist-submenu-popup-item">
    -                    <GeneralViewIcon :meta="{ type: ViewTypes.MAP }" />
    -                    {{ $t('objects.viewType.map') }}
    -                  </div>
    -                </a-menu-item>
    -
                     <a-menu-item
                       v-if="isEeUI && showEEFeatures"
                       data-testid="topbar-view-create-timeline"
    

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