Skip to content

Commit a59e38d

Browse files
committed
Fix security issues flagged in PR #593 code scanning.
Signed-off-by: lakshmj <[email protected]>
1 parent fedf89e commit a59e38d

3 files changed

Lines changed: 79 additions & 16 deletions

File tree

examples/nslaslicense_offline.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,22 @@
2020
- name: Display license result
2121
ansible.builtin.debug:
2222
var: lic_result
23+
24+
- name: Apply offline LAS license with host key checking disabled (trusted isolated environment)
25+
delegate_to: localhost
26+
netscaler.adc.nslaslicense_offline:
27+
nsip: "{{ nsip }}"
28+
nitro_user: "{{ nitro_user }}"
29+
nitro_pass: "{{ nitro_pass }}"
30+
nitro_protocol: "{{ nitro_protocol | default('https') }}"
31+
validate_certs: false
32+
host_key_checking: false
33+
request_pem: CNS_V10000_SERVER
34+
request_ed: Premium
35+
is_fips: false
36+
las_secrets_json: /path/to/las_secrets.json
37+
register: lic_result_no_hkc
38+
39+
- name: Display license result (no host key checking)
40+
ansible.builtin.debug:
41+
var: lic_result_no_hkc

plugins/module_utils/las_utils.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import os
1212
import re
1313
import subprocess
14+
import tempfile
1415
import uuid
1516

1617
from ansible.module_utils.urls import open_url
@@ -207,7 +208,8 @@ def build_multipart(fields, files):
207208
class LASClient:
208209
"""Client for the LAS (License Activation Service) cloud API."""
209210

210-
_BEARER_CACHE = "/tmp/r56_bearer"
211+
# Namespaced by effective user ID to avoid insecure shared /tmp file access.
212+
_BEARER_CACHE = os.path.join(tempfile.gettempdir(), "r56_bearer_{0}".format(os.geteuid()))
211213

212214
def __init__(self, lsguid, secret_file):
213215
self.endpoint = "netscalerfixedbw"
@@ -317,9 +319,16 @@ def get_blob_from_las(self, newactivationid, lsfingerprint, output_file, bearer,
317319
# ---------------------------------------------------------------------------
318320

319321

320-
def sftp_get(ip, username, password, remote_path, local_path, loglines):
322+
def sftp_get(ip, username, password, remote_path, local_path, loglines, host_key_checking=True):
321323
ssh = paramiko.SSHClient()
322-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
324+
ssh.load_system_host_keys()
325+
if host_key_checking:
326+
# RejectPolicy raises an error for unknown host keys, preventing silent MITM attacks.
327+
# The ADC device's SSH host key must be present in the control node's known_hosts.
328+
ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
329+
else:
330+
# User explicitly opted out of host key checking (host_key_checking=false).
331+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
323332
sftp = None
324333
try:
325334
ssh.connect(ip, username=username, password=password)
@@ -334,9 +343,16 @@ def sftp_get(ip, username, password, remote_path, local_path, loglines):
334343
ssh.close()
335344

336345

337-
def sftp_put(ip, username, password, local_path, remote_path, loglines):
346+
def sftp_put(ip, username, password, local_path, remote_path, loglines, host_key_checking=True):
338347
ssh = paramiko.SSHClient()
339-
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
348+
ssh.load_system_host_keys()
349+
if host_key_checking:
350+
# RejectPolicy raises an error for unknown host keys, preventing silent MITM attacks.
351+
# The ADC device's SSH host key must be present in the control node's known_hosts.
352+
ssh.set_missing_host_key_policy(paramiko.RejectPolicy())
353+
else:
354+
# User explicitly opted out of host key checking (host_key_checking=false).
355+
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
340356
sftp = None
341357
try:
342358
ssh.connect(ip, port=22, username=username, password=password)
@@ -427,7 +443,7 @@ def check_if_new_api(mapping, release, major, minor):
427443
# ---------------------------------------------------------------------------
428444

429445

430-
def get_offline_request_package(nitro, ip, username, password, local_dir, new_api, loglines):
446+
def get_offline_request_package(nitro, ip, username, password, local_dir, new_api, loglines, host_key_checking=True):
431447
"""Trigger NITRO to generate the NS offline activation request tgz, then SFTP it to local_dir."""
432448
resource = "nslicenseactivationdata?args=usehostname:true" if new_api else "nslicenseactivationdata"
433449
o = nitro.get(resource)
@@ -438,7 +454,7 @@ def get_offline_request_package(nitro, ip, username, password, local_dir, new_ap
438454
return ""
439455

440456
local_path = os.path.join(local_dir, src_file)
441-
sftp_get(ip, username, password, "/nsconfig/license/" + src_file, local_path, loglines)
457+
sftp_get(ip, username, password, "/nsconfig/license/" + src_file, local_path, loglines, host_key_checking)
442458
return src_file
443459

444460

@@ -449,7 +465,13 @@ def get_offline_request_package(nitro, ip, username, password, local_dir, new_ap
449465

450466
def extract_lsguid(file_path, loglines):
451467
dest_dir = os.path.dirname(file_path)
468+
# Validate that file_path is within dest_dir to guard against path traversal.
469+
real_file_path = os.path.realpath(file_path)
470+
real_dest_dir = os.path.realpath(dest_dir)
471+
if not real_file_path.startswith(real_dest_dir + os.sep):
472+
raise RuntimeError("Invalid file path outside temp directory: {0}".format(file_path))
452473
json_file = "ns_offline_activation_request.json"
474+
# shell=False ensures no shell metacharacter interpretation; all args are controlled internally.
453475
cmd = [
454476
"tar",
455477
"-xvf",
@@ -479,12 +501,12 @@ def extract_lsguid(file_path, loglines):
479501

480502
try:
481503
os.remove(json_path)
482-
except Exception:
483-
pass
504+
except Exception as e:
505+
loglines.append("DEBUG: Could not remove temp file {0}: {1}".format(json_path, str(e)))
484506
try:
485507
os.remove(os.path.join(dest_dir, "lasData.tgz"))
486-
except Exception:
487-
pass
508+
except Exception as e:
509+
loglines.append("DEBUG: Could not remove temp file lasData.tgz: {0}".format(str(e)))
488510

489511
lsguid = data["lsguid"]
490512
loglines.append("INFO: Extracted lsguid: {0}".format(lsguid))
@@ -496,8 +518,8 @@ def extract_lsguid(file_path, loglines):
496518
# ---------------------------------------------------------------------------
497519

498520

499-
def apply_license_blob_ns(nitro, ip, username, password, fname, loglines):
500-
sftp_put(ip, username, password, fname, "/nsconfig/license/" + fname, loglines)
521+
def apply_license_blob_ns(nitro, ip, username, password, fname, loglines, host_key_checking=True):
522+
sftp_put(ip, username, password, fname, "/nsconfig/license/" + fname, loglines, host_key_checking)
501523
payload = {
502524
"params": {"action": "apply", "warning": "YES"},
503525
"nslaslicense": {"filename": fname, "filelocation": "/nsconfig/license", "fixedbandwidth": True},

plugins/modules/nslaslicense_offline.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,14 @@
8989
- The file must contain the keys C(ccid), C(client), C(password), C(las_endpoint), and C(cc_endpoint).
9090
type: str
9191
required: true
92+
host_key_checking:
93+
description:
94+
- If C(false), the ADC device's SSH host key will not be validated during SFTP transfers.
95+
- Disabling this is insecure and should only be used in trusted, isolated environments.
96+
- When C(true) (default), the device's SSH host key must be present in the control node's known_hosts file.
97+
Use C(ssh-keyscan <nsip> >> ~/.ssh/known_hosts) to add it.
98+
type: bool
99+
default: true
92100
"""
93101

94102
EXAMPLES = r"""
@@ -127,6 +135,18 @@
127135
request_ed: Standard
128136
las_secrets_json: /etc/netscaler/zmcd_secrets.json
129137
138+
- name: Apply offline LAS license with host key checking disabled (trusted isolated environment)
139+
delegate_to: localhost
140+
netscaler.adc.nslaslicense_offline:
141+
nsip: 10.102.201.230
142+
nitro_user: nsroot
143+
nitro_pass: "{{ nitro_pass }}"
144+
validate_certs: false
145+
host_key_checking: false
146+
request_pem: CNS_8905_SERVER
147+
request_ed: Premium
148+
las_secrets_json: /etc/netscaler/zmcd_secrets.json
149+
130150
"""
131151

132152
RETURN = r"""
@@ -194,6 +214,7 @@ def main():
194214
request_ed=dict(required=True, type="str", choices=["Advanced", "Premium", "Standard"]),
195215
is_fips=dict(type="bool", default=False),
196216
las_secrets_json=dict(required=True, type="str"),
217+
host_key_checking=dict(type="bool", default=True),
197218
)
198219

199220
module = AnsibleModule(
@@ -214,6 +235,7 @@ def main():
214235
request_ed = module.params["request_ed"]
215236
is_fips = module.params["is_fips"]
216237
las_secrets_json = module.params["las_secrets_json"]
238+
host_key_checking = module.params["host_key_checking"]
217239
if username != "nsroot":
218240
module.fail_json(msg="Only the 'nsroot' account is supported. Got: '{0}'".format(username), **result)
219241

@@ -252,7 +274,7 @@ def main():
252274
# Get activation request package from device
253275
temp_dir = os.path.join(tempfile.mkdtemp(prefix="nslas_"), "")
254276
try:
255-
ns_file_name = get_offline_request_package(nitro, ip, username, password, temp_dir, new_api, loglines)
277+
ns_file_name = get_offline_request_package(nitro, ip, username, password, temp_dir, new_api, loglines, host_key_checking)
256278
if not ns_file_name:
257279
module.fail_json(msg="Failed to retrieve activation request package from device", **result)
258280

@@ -264,7 +286,7 @@ def main():
264286
lsguid = extract_lsguid(request_file, loglines)
265287
except Exception as e:
266288
loglines.append("WARNING: First parse attempt failed ({0}), re-downloading package".format(str(e)))
267-
ns_file_name = get_offline_request_package(nitro, ip, username, password, temp_dir, new_api, loglines)
289+
ns_file_name = get_offline_request_package(nitro, ip, username, password, temp_dir, new_api, loglines, host_key_checking)
268290
if not ns_file_name:
269291
module.fail_json(msg="Re-download of activation request package failed", **result)
270292
request_file = os.path.join(temp_dir, ns_file_name)
@@ -276,7 +298,7 @@ def main():
276298
module.fail_json(msg="Failed to generate offline license token from LAS", **result)
277299

278300
# Apply license blob to device
279-
apply_license_blob_ns(nitro, ip, username, password, output_file, loglines)
301+
apply_license_blob_ns(nitro, ip, username, password, output_file, loglines, host_key_checking)
280302

281303
result["changed"] = True
282304
result["output_file"] = output_file

0 commit comments

Comments
 (0)