← All security articles
CRITICALWashington+

Knowledge Base Article Access Control Exposure

Domain 1: Access Control & Identity Security·

Knowledge Base articles are not protected by the table ACLs that guard the rest of your instance. Access is decided by User Criteria applied at the Knowledge Base level, so one permissively configured KB exposes every article inside it — and because the criteria sit above the article, per-article restrictions do not save you. If a KB is reachable through a public Service Portal, unauthenticated visitors can read its contents.

What This Is

ServiceNow Knowledge Bases (KBs) store internal documentation — login procedures, HR policies, system architecture diagrams, troubleshooting guides, and operational runbooks. Organizations use KBs both for external self-service (public-facing support articles) and internal knowledge sharing (restricted content for employees).

The access control model for KB articles is fundamentally different from standard table ACLs, which makes it uniquely prone to misconfiguration:

  1. KB articles use User Criteria (UC) for access control, not standard ACLs. This means the UserIsAuthenticated Security Attribute that ServiceNow added to standard ACLs in 2023 does NOT protect KB articles.

  2. User Criteria operates at the Knowledge Base level, not the article level. If a KB is configured with permissive UC (e.g., "Any User" or "Guest User"), every article in that KB is exposed.

  3. Public-facing Service Portal widgets can query KB content. Even if the main instance requires SSO, if a Service Portal widget with KB access is publicly accessible, unauthenticated users can retrieve article content.

  4. KB article IDs are sequential and predictable. Article numbers follow the format KB0000001, KB0000002, etc. An attacker can enumerate all articles by incrementing the ID.

How KB Access Control Works

User requests KB article
  → ServiceNow checks: Does the Knowledge Base have "Can Read" User Criteria?
    → If UC includes "Any User" or "Guest": Article returned without authentication
    → If UC includes specific roles/groups: Role check performed
    → If no UC defined: Falls back to KB-level "Who can read" setting
  → If article passes UC check:
    → ServiceNow does NOT additionally check table-level ACLs on kb_knowledge
    → The UC check IS the access control

This means:

Scale of the Problem

Independent security research has demonstrated that approximately 45% of enterprise ServiceNow instances tested had unintentionally exposed KB articles to unauthenticated access. Organizations with multiple ServiceNow instances showed consistent misconfiguration across all instances, suggesting the issue is systemic rather than accidental.

Why This Is Dangerous

Attack Scenario: KB Article Enumeration and Data Exfiltration

Precondition: At least one Knowledge Base has User Criteria that includes "Any User" or "Guest User" in the "Can Read" setting. A public-facing Service Portal exists with a KB-related widget.

Attack chain:

  1. Discovery: Attacker identifies a ServiceNow Service Portal (typically at https://company.service-now.com/sp or a custom domain). No authentication required to reach the portal.

  2. Widget Identification: Attacker identifies KB-related widgets on the portal. Common widgets that expose KB content:

    • kb-article-page — displays individual articles
    • kb-search — full-text search across KB content
    • kb-category-page — browse KB by category
    • sc-category — may include KB article links
    • widget-modal — can proxy requests to other widgets including KB widgets
  3. Brute-Force Enumeration: KB article IDs are sequential (KB0000001 through KB9999999). Using automated tools, the attacker sends requests for each article ID:

    GET /sp?id=kb_article&sys_id=<article_sys_id>
    

    Or via the KB API:

    GET /api/now/table/kb_knowledge
      ?sysparm_query=number=KB0000001
      &sysparm_fields=number,short_description,text,kb_knowledge_base
    
  4. Content Extraction: For each accessible article, extract full content including:

    • Article body text (may contain credentials, connection strings, internal IPs)
    • Attached files (PDFs, diagrams, config files)
    • Article metadata (author, category, publication date)
    • Comments and feedback
  5. Intelligence Gathering: Exposed KB articles typically contain:

    • Credentials and tokens: Login procedures that include actual usernames/passwords, API keys, service account credentials
    • Infrastructure details: Network diagrams, IP ranges, server names, database connection strings
    • Internal procedures: Incident response playbooks, escalation paths, executive contact information
    • HR policies: Compensation structures, disciplinary procedures, investigation protocols
    • System architecture: Integration diagrams, data flow charts, vendor access details
    • Security configurations: Firewall rules, VPN setup guides, MFA bypass procedures

Impact: A single misconfigured Knowledge Base can expose years of accumulated institutional knowledge. Unlike a database breach that exposes structured data, KB articles contain context-rich documentation that gives attackers a deep understanding of the organization's operations, systems, and people.

Attack Scenario: Widget Proxy Bypass

Precondition: KB User Criteria has been tightened, but the widget-modal or similar proxy widget remains publicly accessible.

Attack chain:

  1. Attacker discovers that direct KB article access is restricted
  2. Attacker identifies widget-modal widget on the Service Portal (this widget proxies requests to other widgets)
  3. Attacker uses widget-modal to invoke KB widgets indirectly, passing parameters through the proxy
  4. The proxy widget processes the request in the context of the portal (public), bypassing the UC check intended for the KB widget
  5. Article content is returned through the proxy

This bypass was documented in independent security research and ServiceNow has since modified some widget behaviors, but custom or outdated portal configurations may still be vulnerable.

Attack Scenario: Cross-Instance Knowledge Leakage

Precondition: Organization has multiple ServiceNow instances (production, staging, dev, sandbox) with cloned KB content.

Attack chain:

  1. Production instance has properly configured KB User Criteria
  2. Sub-production instances were cloned from production but KB UC was not verified post-clone
  3. Sub-production instances are often less restricted (no SSO, broader network access, test accounts with weak passwords)
  4. Attacker accesses sub-production KB articles that mirror production content
  5. Credentials and infrastructure details in KB articles are often identical between production and sub-production

How to Detect

Quick Manual Check

Step 1: Identify all Knowledge Bases and their User Criteria

Navigate to: Knowledge > Administration > Knowledge Bases
URL: https://<instance>.service-now.com/kb_knowledge_base_list.do

For each Knowledge Base, check:
  - "Who can read" field → Should NOT be "Any User" or include "Guest"
  - "Who can contribute" field → Should require specific roles
  - "Who can manage" field → Should be restricted to KB admins

Step 2: Check User Criteria definitions

Navigate to: User Administration > User Criteria
URL: https://<instance>.service-now.com/user_criteria_list.do

Filter: Active = true
Check each UC for:
  - "Match All" conditions that include role "public" or user "Guest"
  - "Any User" UC — which KBs reference this?

Step 3: Test unauthenticated access

Open an incognito/private browser window (not logged into ServiceNow) and try:

https://<instance>.service-now.com/sp?id=kb_article&sys_id=<known_article_sys_id>
https://<instance>.service-now.com/kb_view.do?sysparm_article=KB0000001

If articles load without login, you have exposure.

Comprehensive Detection Script

/*
 * ACL-009 Detection Script
 * Identifies Knowledge Bases with overly permissive User Criteria
 *
 * Run as: Background script with admin role
 * Impact: Read-only, safe for production
 * Versions: Orlando+
 * Requires: com.glideapp.knowledge plugin
 */

gs.info('=== ACL-009: KB ACCESS CONTROL AUDIT ===');
gs.info('');

// --- Platform-level KB access posture (sets the fail-open vs fail-closed default) ---
// A KB with no "Can Read" User Criteria fails OPEN unless block_access_with_no_user_criteria
// is 'true'. allow_all_for_unauthenticated gates unauthenticated portal reads. Read-only.
var blockNoUc = gs.getProperty('glide.knowman.block_access_with_no_user_criteria', 'NOT SET');
var allowUnauth = gs.getProperty('glide.communicate.allow_all_for_unauthenticated', 'NOT SET');
gs.info('Platform KB access posture:');
gs.info('  glide.knowman.block_access_with_no_user_criteria = ' + blockNoUc + ' (expected: true — no-UC KBs fail closed)');
gs.info('  glide.communicate.allow_all_for_unauthenticated  = ' + allowUnauth + ' (expected: false — blocks unauthenticated portal access)');
if (blockNoUc !== 'true') {
    gs.info('  [WARNING] no-UC KBs default to OPEN — every "NO USER CRITERIA" finding below is exploitable');
}
gs.info('');

var findings = [];

// Check all active Knowledge Bases
var kb = new GlideRecord('kb_knowledge_base');
kb.addQuery('active', true);
kb.query();

while (kb.next()) {
    var kbName = kb.getValue('title');
    var kbSysId = kb.getUniqueValue();

    // Count articles in this KB
    var articleCount = new GlideAggregate('kb_knowledge');
    articleCount.addQuery('kb_knowledge_base', kbSysId);
    articleCount.addQuery('workflow_state', 'published');
    articleCount.addAggregate('COUNT');
    articleCount.query();
    var count = 0;
    if (articleCount.next()) {
        count = parseInt(articleCount.getAggregate('COUNT'));
    }

    // Check "Can Read" User Criteria
    var ucRead = new GlideRecord('kb_uc_can_read_mtom');
    ucRead.addQuery('kb_knowledge_base', kbSysId);
    ucRead.query();

    var readCriteria = [];
    var hasPublicRead = false;

    while (ucRead.next()) {
        var ucGr = new GlideRecord('user_criteria');
        if (ucGr.get(ucRead.getValue('user_criteria'))) {
            var ucName = ucGr.getValue('name');
            readCriteria.push(ucName);

            // Check if this UC grants public/guest access
            if (ucName === 'Any User' ||
                ucName === 'Guest' ||
                ucName.toLowerCase().indexOf('public') >= 0 ||
                ucName.toLowerCase().indexOf('guest') >= 0 ||
                ucName.toLowerCase().indexOf('anonymous') >= 0) {
                hasPublicRead = true;
            }

            // Check UC role assignments
            var ucRole = ucGr.getValue('role');
            if (ucRole) {
                var roleGr = new GlideRecord('sys_user_role');
                if (roleGr.get(ucRole)) {
                    if (roleGr.getValue('name') === 'public' ||
                        roleGr.getValue('name') === 'guest') {
                        hasPublicRead = true;
                    }
                }
            }
        }
    }

    // Check if no User Criteria defined (may default to open)
    var noUcDefined = readCriteria.length === 0;

    if (hasPublicRead || noUcDefined) {
        var severity = count > 100 ? 'CRITICAL' : count > 10 ? 'HIGH' : 'MEDIUM';

        findings.push({
            severity: severity,
            kb_name: kbName,
            kb_sys_id: kbSysId,
            article_count: count,
            read_criteria: readCriteria.join(', ') || 'NONE DEFINED',
            has_public: hasPublicRead,
            no_uc: noUcDefined
        });
    }
}

// Output results
gs.info('Knowledge Bases with potential public exposure:');
gs.info('');

for (var f = 0; f < findings.length; f++) {
    var finding = findings[f];
    var issue = finding.has_public ? 'PUBLIC USER CRITERIA' : 'NO USER CRITERIA DEFINED';

    gs.info('[' + finding.severity + '] ' +
        'KB: "' + finding.kb_name + '"' +
        ' | Published articles: ' + finding.article_count +
        ' | Issue: ' + issue +
        ' | Read UC: ' + finding.read_criteria);
}

gs.info('');
gs.info('Total KBs with exposure: ' + findings.length);
gs.info('');

// Check for sensitive content in exposed KBs
if (findings.length > 0) {
    gs.info('--- Checking exposed KBs for sensitive content patterns ---');

    var sensitivePatterns = [
        'password', 'credential', 'token', 'api key', 'apikey',
        'secret', 'connection string', 'ssh', 'private key',
        'vpn', 'firewall', 'ip address', 'subnet',
        'salary', 'ssn', 'social security',
        'admin', 'root', 'service account'
    ];

    for (var i = 0; i < findings.length; i++) {
        var kbId = findings[i].kb_sys_id;
        var kbTitle = findings[i].kb_name;

        for (var p = 0; p < sensitivePatterns.length; p++) {
            var pattern = sensitivePatterns[p];
            var search = new GlideRecord('kb_knowledge');
            search.addQuery('kb_knowledge_base', kbId);
            search.addQuery('workflow_state', 'published');
            search.addQuery('text', 'CONTAINS', pattern);
            search.setLimit(1);
            search.query();

            if (search.next()) {
                gs.info('  WARNING: KB "' + kbTitle +
                    '" contains articles matching "' + pattern + '"');
            }
        }
    }
}

gs.info('');
gs.info('NEXT STEPS:');
gs.info('1. For each exposed KB, add appropriate User Criteria (see remediation)');
gs.info('2. Check Service Portal widgets for public KB access (see ACL-010)');
gs.info('3. Audit KB articles for credentials and sensitive data (see DATA-006)');
gs.info('4. Test unauthenticated access from an external browser');

Remediation

Step 1: Set Restrictive User Criteria on All Knowledge Bases

For each Knowledge Base, configure "Can Read" User Criteria:

Navigate to: Knowledge > Administration > Knowledge Bases > [Your KB]

"Who can read" section:
  1. Remove "Any User" and "Guest" User Criteria
  2. Add specific UC for intended audiences:
     - Internal employees: Create UC with condition "Active = true AND Role != Guest"
     - Specific teams: Create UC with group membership conditions
     - External customers: Create UC with specific portal user conditions

"Who can contribute" section:
  - Restrict to KB authors/editors only

"Who can manage" section:
  - Restrict to KB administrators only

Step 2: Create a Restrictive Default User Criteria

/*
 * Create a "Authenticated Internal Users Only" User Criteria
 * Run once in background script
 */

var uc = new GlideRecord('user_criteria');
uc.initialize();
uc.setValue('name', 'Authenticated Internal Users');
uc.setValue('active', true);
// Add conditions: user must be active AND have an internal role
uc.setValue('script',
    'var result = false;\n' +
    'if (gs.isLoggedIn() && !gs.hasRole("public") && !gs.hasRole("guest")) {\n' +
    '    result = true;\n' +
    '}\n' +
    'result;'
);
uc.insert();
gs.info('Created UC: ' + uc.getUniqueValue());

Step 3: Implement a Business Rule to Block Unauthenticated KB Access

/*
 * Business Rule: Block unauthenticated KB article access
 *
 * Table: kb_knowledge
 * When: Before query
 * Active: true
 * Advanced: true
 */

(function executeRule(current, previous) {
    if (gs.hasRole('public') || !gs.isLoggedIn()) {
        // Check if this KB is explicitly marked as public
        var kb = new GlideRecord('kb_knowledge_base');
        if (kb.get(current.getValue('kb_knowledge_base'))) {
            var isPublic = kb.getValue('u_public_access_approved') == 'true';
            if (!isPublic) {
                current.addQuery('sys_id', 'invalid_id_block_all');
                gs.info('[SECURITY] Blocked unauthenticated KB access attempt');
            }
        }
    }
})(current, previous);

Step 4: Audit KB Articles for Sensitive Content

Before securing KBs, identify articles that contain credentials or sensitive data that should be removed regardless of access controls:

/*
 * Scan KB articles for sensitive content
 * Run as background script
 */

var sensitivePatterns = {
    'CREDENTIAL': ['password:', 'passwd:', 'pwd=', 'token:', 'api_key:', 'apikey=',
                   'secret:', 'private_key', 'ssh-rsa', 'BEGIN RSA', 'BEGIN CERTIFICATE'],
    'NETWORK': ['10.0.', '172.16.', '192.168.', 'subnet', 'firewall rule',
                'vpn config', 'dns server'],
    'PII': ['ssn:', 'social security', 'date of birth', 'salary:',
            'bank account', 'routing number']
};

var results = {};

for (var category in sensitivePatterns) {
    var patterns = sensitivePatterns[category];
    results[category] = [];

    for (var p = 0; p < patterns.length; p++) {
        var gr = new GlideRecord('kb_knowledge');
        gr.addQuery('workflow_state', 'published');
        gr.addQuery('text', 'CONTAINS', patterns[p]);
        gr.query();

        while (gr.next()) {
            var articleKey = gr.getValue('number');
            if (results[category].indexOf(articleKey) < 0) {
                results[category].push(articleKey);
                gs.info('[' + category + '] Article ' + articleKey +
                    ': "' + gr.getValue('short_description') + '"' +
                    ' | KB: ' + gr.getDisplayValue('kb_knowledge_base') +
                    ' | Pattern: "' + patterns[p] + '"');
            }
        }
    }
}

gs.info('');
gs.info('Summary:');
for (var cat in results) {
    gs.info('  ' + cat + ': ' + results[cat].length + ' articles with potential sensitive content');
}

Step 5: Post-Remediation Verification

/*
 * Verify KB access controls are effective
 * Tests by impersonating guest user
 */

var kbs = new GlideRecord('kb_knowledge_base');
kbs.addQuery('active', true);
kbs.query();

gs.info('=== KB Access Control Verification ===');

// Impersonate guest user
var guestUser = new GlideRecord('sys_user');
guestUser.addQuery('user_name', 'guest');
guestUser.query();

if (guestUser.next()) {
    var imp = new GlideImpersonate();
    imp.impersonate(guestUser.getUniqueValue());

    var accessible = new GlideRecord('kb_knowledge');
    accessible.addQuery('workflow_state', 'published');
    accessible.query();
    var guestCount = accessible.getRowCount();

    imp.unimpersonate();

    gs.info('Articles accessible to Guest user: ' + guestCount);
    if (guestCount > 0) {
        gs.info('WARNING: Guest user can still access ' + guestCount + ' KB articles');
    } else {
        gs.info('PASS: Guest user cannot access any KB articles');
    }
}

// Also verify via REST (simulates external attacker)
gs.info('');
gs.info('MANUAL TEST REQUIRED:');
gs.info('Open incognito browser and try:');
gs.info('  https://<instance>.service-now.com/sp?id=kb_article&sys_id=<known_sys_id>');
gs.info('  https://<instance>.service-now.com/kb_view.do?sysparm_article=KB0000001');
gs.info('If any content loads without login, exposure persists.');

Regulatory Impact

NIS2 Mapping

Article Requirement How KB Exposure Violates It Evidence After Fix
Art.21§2(a) Risk analysis and IS security policies Internal documentation publicly accessible; no risk assessment of KB content classification KB access audit completed, UC configured per KB, content sensitivity review
Art.21§2(i) HR security, access control policies HR policies, procedures, and potentially PII in KB articles accessible without authentication All HR-related KBs restricted to authenticated employees with appropriate roles

DORA Mapping

Article Requirement How KB Exposure Violates It Evidence After Fix
Art.9§1 ICT risk management framework KB articles containing system credentials and architecture details accessible publicly Credential scan completed, all ICT-related KBs restricted, sensitive content removed
Art.9§4(c) Anomalous activity detection No logging or alerting on bulk KB article access attempts Business Rule logging unauthenticated access attempts, alert on enumeration patterns

ISO 27001:2022 Mapping

Control Requirement How KB Exposure Violates It Evidence After Fix
A.5.15 Access control Knowledge Base content not restricted based on information security requirements User Criteria configured per KB with documented access requirements
A.8.3 Information access restriction Published articles accessible without access control verification UC-based access control enforced, business rule blocks unauthenticated access
A.5.12 Classification of information KB articles not classified by sensitivity — public and confidential content in same KBs Content classification completed, separate KBs for public vs. internal content

GDPR Mapping

Article Requirement How KB Exposure Violates It Impact
Art.32§1(b) Ongoing confidentiality of processing systems PII in KB articles (employee data, customer information) accessible without authentication Must notify DPO; assess if personal data was actually accessed during exposure window
Art.5§1(f) Integrity and confidentiality Personal data in KB articles not protected with appropriate security measures Demonstrates inadequate technical measures; potential Art.33 breach notification

Expert Notes

Practitioner annotations pending — article content has been technically validated.