Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
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
7 changes: 6 additions & 1 deletion 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 @@ -40,7 +40,12 @@ 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_URL=<Full URL for the Tenable.SC host, including port (defaults to :443 if the client doesn't specify one). For multiple allowed hosts, wrap the value in quotes and separate with newlines, e.g.
```
TENABLE_HOST_URL='https://a.example.com
https://b.example.com:8443'
```
Client-supplied host_url values not matching an entry in this list are rejected>

# Authentication

Expand Down
41 changes: 36 additions & 5 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 All @@ -52,13 +52,44 @@
}
}

getTenableHostUrl(): string {
// Newline-separated allowlist of Tenable.SC hosts for the login/proxy endpoints
// Entries must include a protocol and may include a port; malformed entries throw at startup.
getTenableHostUrl(): string[] {
const tenable_host_url = this.get('TENABLE_HOST_URL');
if (tenable_host_url !== undefined) {
return tenable_host_url;
} else {
return '';
if (tenable_host_url === undefined) {
return [];
}
return tenable_host_url
.split('\n')
.map((url) => url.trim())
.filter((url) => url.length > 0)
.map((url) => {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
throw new Error(
`Invalid TENABLE_HOST_URL entry "${url}": must be a complete URL including protocol (e.g. https://example.com)`
);
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
throw new Error(
`Invalid TENABLE_HOST_URL entry "${url}": protocol must be http or https`
);
}
// Only protocol + hostname + port are allowed; ports are permitted since
// Tenable.SC may run on a non-default port.
const hasExtra =

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.

both of the hasExtra processing areas need to exclude everything aside from hostname + protocol + optionally port

considering that this processing code is the same, maybe we can extract out to some common utility function and use it in both places.

(parsed.pathname !== '' && parsed.pathname !== '/') ||
parsed.search !== '' ||
parsed.hash !== '';
if (hasExtra) {
throw new Error(
`Invalid TENABLE_HOST_URL entry "${url}": must contain only a protocol, hostname, and optional port (no path, query, or fragment)`
);
}
return parsed.origin;
Comment thread
DMedina6 marked this conversation as resolved.
});
}

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

Check warning on line 125 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 126 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 +135,8 @@
sslCert = this.get('DATABASE_SSL_CERT');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_CERT')!).isFile()) {

Check warning on line 138 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 139 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 +148,8 @@
sslCA = this.get('DATABASE_SSL_CA');
} else {
// Verify file exists
if (fs.statSync(this.get('DATABASE_SSL_CA')!).isFile()) {

Check warning on line 151 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 152 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 +193,7 @@
return false;
} else {
const pattern =
/^(?:([^:\/?#\s]+):\/{2})?(?:([^@\/?#\s]+)@)?([^\/?#\s]+)?(?:\/([^?#\s]*))?(?:[?]([^#\s]+))?\S*$/;

Check warning on line 196 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
2 changes: 1 addition & 1 deletion apps/backend/src/config/config.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class ConfigService {
return this.appConfig.getSplunkHostUrl();
}

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

Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/config/dto/startup-settings.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class StartupSettingsDto implements IStartupSettings {
readonly ldap: boolean;
readonly registrationEnabled: boolean;
readonly localLoginEnabled: boolean;
readonly tenableHostUrl: string;
readonly tenableHostUrl: string[];
readonly forceTenableFrontend: boolean;
readonly splunkHostUrl: string;

Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ async function bootstrap() {
"'self'",
'https://api.github.com',
'https://sts.amazonaws.com',
configService.getTenableHostUrl(),
...configService.getTenableHostUrl(),
configService.getSplunkHostUrl()
].filter((source) => source)
}
Expand Down
Loading
Loading