Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
53cfe2d
Fix SSRF in Tenable integration by requiring auth and validating host…
DMedina6 Aug 7, 2026
f2d6a86
Propagate only sanitized value
DMedina6 Aug 10, 2026
6dd3ada
Tenable host_url allowlist validation
DMedina6 Aug 10, 2026
a1322ae
Import ConfigModule, backwards compatibility with TENABLE_HOST_URL va…
DMedina6 Aug 10, 2026
e68e3e5
consolidate tenable host URL env vars to single variable, allow both …
DMedina6 Aug 14, 2026
afcda97
Validate and trim at config load; require explicit protocol
DMedina6 Aug 14, 2026
c452f75
Clean up
DMedina6 Aug 14, 2026
6c40c83
Use error code 400 with custom set code for unallowed host URL instea…
DMedina6 Aug 14, 2026
13ca3c3
Merge branch 'master' of https://github.com/mitre/heimdall2 into tena…
DMedina6 Aug 14, 2026
1569459
Address sonarqube finding: extract error mappings to private method
DMedina6 Aug 17, 2026
3d3c39b
Merge branch 'master' into tenable-url-security
DMedina6 Aug 17, 2026
7d4eebf
Review comment adjustments
DMedina6 Aug 18, 2026
c685679
Merge branch 'tenable-url-security' of https://github.com/mitre/heimd…
DMedina6 Aug 18, 2026
0e0ab24
Merge branch 'master' of https://github.com/mitre/heimdall2 into tena…
DMedina6 Aug 18, 2026
694ab1d
Prevent tenable session credentials from being reused by another Heim…
DMedina6 Aug 18, 2026
71bbddd
Do not persist Tenable credentials in local storage
DMedina6 Aug 18, 2026
d0fbc3b
Move URL validation to new shared utility function
DMedina6 Aug 18, 2026
77c178f
Merge branch 'master' into tenable-url-security
Amndeep7 Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/backend/.env-example

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll also need to update the envvar wiki page

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll also probably need to update the heimdall helm chart

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll also probably need to update the tenable integration part of the integrations wiki page

Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ NGINX_HOST=<Templated out as the 'server_name' for the NGINX configuration (no d
## External interfaces
SPLUNK_HOST_URL=<The full Uniform Resource Locator (URL) without the port for the Splunk host (no default, must be set if connecting to Splunk)>
TENABLE_HOST_URL=<The full Uniform Resource Locator (URL) without the port for the Tenable.SC host (no default, must be set if connecting to Tenable)>
TENABLE_HOST_URLS=<Comma-separated allowlist of full Tenable.SC host URLs permitted for login (e.g. https://a.example.com,https://b.example.com). Client-supplied host_url values not in this list are rejected>
Comment thread
DMedina6 marked this conversation as resolved.
Outdated
Comment thread
DMedina6 marked this conversation as resolved.
Outdated

# Authentication

Expand Down
13 changes: 13 additions & 0 deletions apps/backend/config/app_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@
}

set(key: string, value: string | undefined): void {
this.envConfig[key] = value;

Check warning on line 30 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Generic Object Injection Sink
}

get(key: string): string | undefined {
return process.env[key] || this.envConfig[key];

Check warning on line 34 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Generic Object Injection Sink

Check warning on line 34 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Generic Object Injection Sink
}

getExternalUrl(): string {
Expand Down Expand Up @@ -61,6 +61,19 @@
}
}

// Comma-separated allowlist of Tenable.SC hosts permitted for the login/proxy endpoints.
Comment thread
DMedina6 marked this conversation as resolved.
Outdated
getTenableHostUrls(): string[] {
const tenable_host_urls = this.get('TENABLE_HOST_URLS');
if (tenable_host_urls !== undefined) {
return tenable_host_urls
.split(',')
.map((url) => url.trim())
.filter((url) => url.length > 0);
} else {
return [];
}
}
Comment thread
DMedina6 marked this conversation as resolved.
Outdated

getDatabaseName(): string {
const databaseName = this.get('DATABASE_NAME');
const nodeEnvironment = this.get('NODE_ENV');
Expand Down Expand Up @@ -91,8 +104,8 @@
sslKey = this.get('DATABASE_SSL_KEY');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_KEY')!).isFile()) {

Check warning on line 107 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Found statSync from package "fs" with non literal argument at index 0
sslKey = fs.readFileSync(this.get('DATABASE_SSL_KEY')!);

Check warning on line 108 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Found readFileSync from package "fs" with non literal argument at index 0
} else {
throw new Error('SSL Key file does not exist');
}
Expand All @@ -104,8 +117,8 @@
sslCert = this.get('DATABASE_SSL_CERT');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_CERT')!).isFile()) {

Check warning on line 120 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Found statSync from package "fs" with non literal argument at index 0
sslCert = fs.readFileSync(this.get('DATABASE_SSL_CERT')!);

Check warning on line 121 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Found readFileSync from package "fs" with non literal argument at index 0
} else {
throw new Error('SSL Cert file does not exist');
}
Expand All @@ -117,8 +130,8 @@
sslCA = this.get('DATABASE_SSL_CA');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_CA')!).isFile()) {

Check warning on line 133 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Found statSync from package "fs" with non literal argument at index 0
sslCA = fs.readFileSync(this.get('DATABASE_SSL_CA')!);

Check warning on line 134 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Found readFileSync from package "fs" with non literal argument at index 0
} else {
throw new Error('SSL CA file does not exist');
}
Expand Down Expand Up @@ -162,7 +175,7 @@
return false;
} else {
const pattern =
/^(?:([^:\/?#\s]+):\/{2})?(?:([^@\/?#\s]+)@)?([^\/?#\s]+)?(?:\/([^?#\s]*))?(?:[?]([^#\s]+))?\S*$/;

Check warning on line 178 in apps/backend/config/app_config.ts

View workflow job for this annotation

GitHub Actions / build

Unsafe Regular Expression
const matches = url.match(pattern);

if (matches === null) {
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export class ConfigService {
return this.appConfig.getTenableHostUrl();
}

getTenableHostUrls(): string[] {
return this.appConfig.getTenableHostUrls();
}

getDbConfig(): SequelizeOptions {
return this.appConfig.getDbConfig();
}
Expand Down
42 changes: 40 additions & 2 deletions apps/backend/src/tenable/tenable.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
Body,
HttpException,
HttpStatus,
All
All,
UseGuards
} from '@nestjs/common';
import {TenableService} from './tenable.service';
import {JwtAuthGuard} from '../guards/jwt-auth.guard';
import {ConfigService} from '../config/config.service';
import axios from 'axios';
import {Request, Response} from 'express';

Expand All @@ -30,8 +33,32 @@
// It allows users to log in with their Tenable credentials and then proxies all subsequent requests
// to the Tenable API, handling authentication via session storage.
@Controller('api/tenable')
@UseGuards(JwtAuthGuard)
export class TenableController {
constructor(private readonly tenableService: TenableService) {}
constructor(
private readonly tenableService: TenableService,
private readonly configService: ConfigService
) {}

// Confirms host_url's origin matches an entry in the TENABLE_HOST_URLS allowlist.
private isAllowedHostUrl(host_url: string): boolean {
const allowlist = this.configService.getTenableHostUrls();
if (allowlist.length === 0) {
return false;
}
try {
const requestedOrigin = new URL(host_url).origin;
return allowlist.some((allowed) => {
try {
return new URL(allowed).origin === requestedOrigin;
} catch {
return false;
}
});
} catch {
return false;
}
}

@Post('login')
/**
Expand All @@ -42,7 +69,7 @@
* @returns An object indicating success and the authenticated user's data from Tenable.
* @throws {HttpException} If any credentials are missing or if authentication fails.
*/
async login(

Check failure on line 72 in apps/backend/src/tenable/tenable.controller.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=mitre_heimdall2&issues=AZ_sz9sRrU821wARvZrE&open=AZ_sz9sRrU821wARvZrE&pullRequest=8510
@Req() req: Request,
@Body() body: {host_url: string; accesskey: string; secretkey: string}
) {
Expand All @@ -52,6 +79,17 @@
throw new HttpException('Missing credentials', HttpStatus.BAD_REQUEST);
}

if (!this.isAllowedHostUrl(host_url)) {
throw new HttpException(
Comment thread
Amndeep7 marked this conversation as resolved.
{
status: HttpStatus.FORBIDDEN,
message: 'Tenable host URL is not in the configured allowlist',
code: 'HOST_NOT_ALLOWED'
},
HttpStatus.FORBIDDEN
);
}

try {
// This helps prevent double slashes in the resulting URL if host_url ends with a slash.
const fullUrl = `${host_url.replace(/\/$/, '')}/rest/currentUser`;
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/src/tenable/tenable.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Module} from '@nestjs/common';
import {TenableController} from './tenable.controller';
import {TenableService} from './tenable.service';
import {ConfigService} from '../config/config.service';

Comment thread
DMedina6 marked this conversation as resolved.
// NestJS module definition for the Tenable proxy feature.
// Registers the controller and service needed for routing Tenable requests.
Expand All @@ -9,6 +10,6 @@ import {TenableService} from './tenable.service';
// Handles HTTP requests related to Tenable
controllers: [TenableController],
// Provides logic for proxying and interacting with Tenable API
providers: [TenableService]
providers: [TenableService, ConfigService]
})
export class TenableModule {}
Empty file added problem-context.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

delete this

Empty file.
Loading