commit aa63e565d20ad34ab98904d6b1367a55caaca405
parent cff275d906977393de1680ffa5a6288f0778683e
Author: Asher Morgan <asher@ashermorgan.net>
Date: Sun, 16 Aug 2026 17:25:52 -0700
Handle arbitrary (whitelisted) Ansible args
Diffstat:
3 files changed, 57 insertions(+), 7 deletions(-)
diff --git a/aoh/install.py b/aoh/install.py
@@ -2,6 +2,7 @@
import base64
import subprocess
+import sys
import time
import requests # TODO: eliminate dependency?
@@ -50,9 +51,12 @@ def fetch(args):
def main():
res = requests.post(f'{API}/runners/', json={
- 'host': 'myhost',
+ 'args': ' '.join(sys.argv[1:]),
})
- assert res.status_code == 201
+ if res.status_code != 201:
+ print(res.json()['err'])
+ sys.exit(1)
+
cookies = res.cookies
job_url = f'{API}{res.headers['Location']}'
@@ -64,6 +68,9 @@ def main():
print(res['logs'], end='')
req = {}
+ if 'err' in res:
+ print(res['err'])
+ sys.exit(1)
if res['finished']:
break
elif 'exec' in res:
diff --git a/aoh/runner.py b/aoh/runner.py
@@ -14,7 +14,7 @@ class AoHError(Exception):
class AoHRunner:
- def __init__(self, config, playbook, host):
+ def __init__(self, config, playbook, args):
self.id = str(uuid4())
self._dir = tempfile.mkdtemp()
self._LOGS_PATH = f'{self._dir}/artifacts/{self.id}/stdout'
@@ -27,7 +27,7 @@ class AoHRunner:
self._runner = None
try:
- self._start_runner(config, playbook, host)
+ self._start_runner(config, playbook, args)
# We assume that ansible-runner will eventually create its log file
while not os.path.exists(self._LOGS_PATH):
@@ -48,7 +48,7 @@ class AoHRunner:
raise
- def _start_runner(self, config, playbook, host):
+ def _start_runner(self, config, playbook, args):
raw_config = ansible_runner.get_ansible_config(
'dump',
config,
@@ -77,9 +77,9 @@ class AoHRunner:
ident=self.id,
envvars=env,
extravars=vars,
+ cmdline=args,
playbook=playbook,
- limit=host,
# verbosity=3,
quiet=True,
diff --git a/aoh/server.py b/aoh/server.py
@@ -13,6 +13,46 @@ app.secret_key = os.urandom(16)
_DATA = {}
+def _get_opts(cmdline):
+ opts = []
+ for arg in cmdline.split():
+ if arg.startswith('--'):
+ opts += [arg.split('=')[0]]
+ elif arg.startswith('-'):
+ for opt in arg.split('=')[0][1:]:
+ opts += ['-' + opt]
+ return opts
+
+
+def _validate_args(cmdline):
+ # Reject any sign of Jinja expressions
+ if '{{' in cmdline or '{%' in cmdline:
+ return False
+
+ # Enforce whitelisted options. Other options either might not be supported
+ # yet, or may pose security risks.
+ OPT_WHITELIST = [
+ '--check', '-C',
+ '--diff', '-D',
+ '--extra-vars', '-e',
+ '--help', '-h',
+ '--limit', '-l',
+ '--list-tags',
+ '--skip-tags',
+ '--start-at-task',
+ '--step',
+ '--tags', '-t',
+ '--verbose', '-v',
+ '--version',
+ ]
+ for opt in _get_opts(cmdline):
+ if opt not in OPT_WHITELIST:
+ print('bad', opt)
+ return False
+
+ return True
+
+
@app.get('/install')
@app.get('/install.py')
def install():
@@ -21,7 +61,10 @@ def install():
@app.post('/runners/')
def job_new():
- runner = AoHRunner(CONFIG, PLAYBOOK, request.json['host'])
+ if not _validate_args(request.json['args']):
+ return { 'err': 'Bad or banned arguments passed.' }, 400
+
+ runner = AoHRunner(CONFIG, PLAYBOOK, request.json['args'])
_DATA[runner.id] = runner
session['runner'] = runner.id