Repository: ansible/ansible

Issue:
Unarchive module catastrophically changes root directory access rights
##### ISSUE TYPE
 - Bug Report

NOTE: This is rather catastrophic failure.

##### COMPONENT NAME
 - Unarchive module

##### ANSIBLE VERSION
```
ansible 2.2.1.0
  config file = /Users/michal/.ansible.cfg
  configured module search path = Default w/o overrides
```

##### CONFIGURATION
```
[defaults]
allow_world_readable_tmpfiles: true
```

##### OS / ENVIRONMENT
Managed OS: Ubuntu Linux 16.04 LTS

##### SUMMARY
Unarchive with `--strip-components=1` on an archive without any root paths works, but changes access rights to root directory to "root only read write", which breaks all user access.

```yaml
      - name:   "Unpack Apache Spark"                                      
        unarchive:                                                         
          src:         "/home/username/Downloads/spark-2_1_0-bin-hadoop2_7.tgz"
          dest:       "/usr/local/share/spark"                             
          remote_src:  yes                                                 
          mode:        0440                                                
          extra_opts: ['--strip-components=1']
```

##### STEPS TO REPRODUCE
Use the following playbook, and say goodbye to non-root login:
*WARNING* This may disable all your user accounts!

```yaml
  - hosts: all
  - tasks:
      - name: "Create Downloads directory"        
        file:                                     
          path:  /home/username/Downloads         
          state: directory                        
          mode:  0755                             
      - name:   "Download Apache Spark"           
        get_url:                                  
          url:  "http://d3kbcqa49mib13.cloudfront.net/spark-2.1.0-bin-hadoop2.7.tgz"
          dest:             "/home/username/Downloads/spark-2_1_0-bin-hadoop2_7.tgz"
          mode: 0440                              
      - name: "Create Spark directory"            
        file:                                     
          path:  /usr/local/share/spark           
          state: directory                        
          mode:  0755                             
      - name:   "Unpack Apache Spark"             
        unarchive:                                
          src:              "/home/username/Downloads/spark-2_1_0-bin-hadoop2_7.tgz"
          dest:       "/usr/local/share/spark"    
          remote_src:  yes                        
          mode:        0440                       
          extra_opts: ['--strip-components=1', "-C /usr/local/share/spark"]
```

##### EXPECTED RESULTS
Running this playbook, you will see:
```
TASK [Unpack Apache Spark] *****************************************************
 [WARNING]: Module invocation had junk after the JSON data: /bin/sh: 1: sleep: Permission denied

changed: [1.2.3.4]
```
Listing access rights to root directory from root account gives:
```
bash$ ls -alth /
dr--r-----  22 root root 4.0K Sep 15 21:11 .
dr--r-----  22 root root 4.0K Sep 15 21:11 ..
```

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
No idea why and how it happens, but here are Ansible logs at -vvvv
<!--- Paste verbatim command output between quotes below -->
```
TASK [Unpack Apache Spark] *****************************************************
task path: /Users/michal/src/voldemort-vm/config.yml:259
<1.2.3.4> ESTABLISH SSH CONNECTION FOR USER: username
<1.2.3.4> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r 1.2.3.4 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo ~/.ansible/tmp/ansible-tmp-1487068628.77-3385553654911 `" && echo ansible-tmp-1487068628.77-3385553654911="` echo ~/.ansible/tmp/ansible-tmp-1487068628.77-3385553654911 `" ) && sleep 0'"'"''
Using module file /usr/local/Cellar/ansible/2.2.1.0/libexec/lib/python2.7/site-packages/ansible/modules/core/files/stat.py
<1.2.3.4> ESTABLISH SSH CONNECTION FOR USER: username
<1.2.3.4> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r 1.2.3.4 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo ~/.ansible/tmp/ansible-tmp-1487068628.89-18627339563725 `" && echo ansible-tmp-1487068628.89-18627339563725="` echo ~/.ansible/tmp/ansible-tmp-1487068628.89-18627339563725 `" ) && sleep 0'"'"''
<1.2.3.4> PUT /var/folders/b_/psf56c3d1vn34tm8mm3x501r0000gp/T/tmpKU2jxY TO /home/username/.ansible/tmp/ansible-tmp-1487068628.89-18627339563725/stat.py
<1.2.3.4> SSH: EXEC sftp -b - -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r '[1.2.3.4]'
<1.2.3.4> ESTABLISH SSH CONNECTION FOR USER: username
<1.2.3.4> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r 1.2.3.4 '/bin/sh -c '"'"'chmod u+x /home/username/.ansible/tmp/ansible-tmp-1487068628.89-18627339563725/ /home/username/.ansible/tmp/ansible-tmp-1487068628.89-18627339563725/stat.py && sleep 0'"'"''
<1.2.3.4> ESTABLISH SSH CONNECTION FOR USER: username
<1.2.3.4> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r -tt 1.2.3.4 '/bin/sh -c '"'"'sudo -H -S -n -u root /bin/sh -c '"'"'"'"'"'"'"'"'echo BECOME-SUCCESS-gtdoezchpqakpvijkjoqhrvhjsrbrpkg; /usr/bin/python /home/username/.ansible/tmp/ansible-tmp-1487068628.89-18627339563725/stat.py'"'"'"'"'"'"'"'"' && sleep 0'"'"''
Using module file /usr/local/Cellar/ansible/2.2.1.0/libexec/lib/python2.7/site-packages/ansible/modules/core/files/unarchive.py
<1.2.3.4> ESTABLISH SSH CONNECTION FOR USER: username
<1.2.3.4> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r 1.2.3.4 '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo ~/.ansible/tmp/ansible-tmp-1487068629.41-74555029658256 `" && echo ansible-tmp-1487068629.41-74555029658256="` echo ~/.ansible/tmp/ansible-tmp-1487068629.41-74555029658256 `" ) && sleep 0'"'"''
<1.2.3.4> PUT /var/folders/b_/psf56c3d1vn34tm8mm3x501r0000gp/T/tmpdHuMwo TO /home/username/.ansible/tmp/ansible-tmp-1487068629.41-74555029658256/unarchive.py
<1.2.3.4> SSH: EXEC sftp -b - -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r '[1.2.3.4]'
<1.2.3.4> ESTABLISH SSH CONNECTION FOR USER: username
<1.2.3.4> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r 1.2.3.4 '/bin/sh -c '"'"'chmod u+x /home/username/.ansible/tmp/ansible-tmp-1487068629.41-74555029658256/ /home/username/.ansible/tmp/ansible-tmp-1487068629.41-74555029658256/unarchive.py && sleep 0'"'"''
<1.2.3.4> ESTABLISH SSH CONNECTION FOR USER: username
<1.2.3.4> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=username -o ConnectTimeout=10 -o ControlPath=/Users/michal/.ansible/cp/ansible-ssh-%h-%p-%r -tt 1.2.3.4 '/bin/sh -c '"'"'sudo -H -S -n -u root /bin/sh -c '"'"'"'"'"'"'"'"'echo BECOME-SUCCESS-vefpwcwkynrvbpsxfbswcjhycrakqpha; /usr/bin/python /home/username/.ansible/tmp/ansible-tmp-1487068629.41-74555029658256/unarchive.py'"'"'"'"'"'"'"'"' && sleep 0'"'"''
 [WARNING]: Module invocation had junk after the JSON data: /bin/sh: 1: sleep: Permission denied

changed: [1.2.3.4] => {
    "changed": true, 
    "dest": "/usr/local/share/spark", 
    "gid": 0, 
    "group": "root", 
    "handler": "TgzArchive", 
    "invocation": {
        "module_args": {
            "backup": null, 
            "content": null, 
            "copy": true, 
            "creates": null, 
            "delimiter": null, 
            "dest": "/usr/local/share/spark", 
            "directory_mode": null, 
            "exclude": [], 
            "extra_opts": [
                "--strip-components=1"
            ], 
            "follow": false, 
            "force": null, 
            "group": null, 
            "keep_newer": false, 
            "list_files": false, 
            "mode": 288, 
            "original_basename": "spark-2_1_0-bin-hadoop2_7.tgz", 
            "owner": null, 
            "regexp": null, 
            "remote_src": true, 
            "selevel": null, 
            "serole": null, 
            "setype": null, 
            "seuser": null, 
            "src": "/home/username/Downloads/spark-2_1_0-bin-hadoop2_7.tgz", 
            "unsafe_writes": null, 
            "validate_certs": true
        }
    }, 
    "mode": "0755", 
    "owner": "root", 
    "size": 4096, 
    "src": "/home/username/Downloads/spark-2_1_0-bin-hadoop2_7.tgz", 
    "state": "directory", 
    "uid": 0
}
```



Discussion:
cc @dagwieers @pileofrogs
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
There are quite some fixes hiding in an older PR #19015, I would appreciate it if you could use that module and run it with a sufficient number of -vvvvv statements. This should give us the exact command-line and output from gtar.
This affects 2.4.1 as well
And 2.4.2 as I see.

I'm experiencing the same issue. OpenSSH doesn't allow to login after this task run.
I was lucky that it was in docker container so that I can re-provision it. 
Note, For people using ```mode: 0440```,  you most probably want ```mode: 'u=rwX,g=gwX,o='``` instead.

(Presently talking with bcoca about whether this is something that should be fixed in ansible code or is categorizable as something we don't handle [extra_opts is inherently an advanced feature which may not work with ansible]).  I think I have a way to fix this but I need to confirm that the caveats to that strategy are okay.

Resolving patch:
diff --git a/lib/ansible/modules/files/unarchive.py b/lib/ansible/modules/files/unarchive.py
--- a/lib/ansible/modules/files/unarchive.py
+++ b/lib/ansible/modules/files/unarchive.py
@@ -642,9 +642,17 @@ def files_in_archive(self, force_refresh=False):
         for filename in out.splitlines():
             # Compensate for locale-related problems in gtar output (octal unicode representation) #11348
             # filename = filename.decode('string_escape')
-            filename = codecs.escape_decode(filename)[0]
+            filename = to_native(codecs.escape_decode(filename)[0])
+
             if filename and filename not in self.excludes:
-                self._files_in_archive.append(to_native(filename))
+                # We don't allow absolute filenames.  If the user wants to unarchive rooted in "/"
+                # they need to use "dest: '/'".  This follows the defaults for gtar, pax, etc.
+                # Allowing absolute filenames here also causes bugs: https://github.com/ansible/ansible/issues/21397
+                if filename.startswith('/'):
+                    filename = filename[1:]
+
+                self._files_in_archive.append(filename)
+
         return self._files_in_archive
 
     def is_unarchived(self):
@@ -869,6 +877,7 @@ def main():
         # do we need to change perms?
         for filename in handler.files_in_archive:
             file_args['path'] = os.path.join(dest, filename)
+
             try:
                 res_args['changed'] = module.set_fs_attributes_if_different(file_args, res_args['changed'], expand=False)
             except (IOError, OSError) as e:


Test patch: