aboutsummaryrefslogtreecommitdiffstats
path: root/netx/net/sourceforge/jnlp/runtime/JNLPSecurityManager.java
blob: 32a54f83f0ab01613c4e1b9d6ae2b7d10186a6be (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
// Copyright (C) 2001-2003 Jon A. Maxwell (JAM)
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


package net.sourceforge.jnlp.runtime;

import static net.sourceforge.jnlp.runtime.Translator.R;

import java.awt.Frame;
import java.awt.Window;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.ref.WeakReference;
import java.net.SocketPermission;
import java.security.AllPermission;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.Permission;
import java.security.PrivilegedAction;
import java.security.SecurityPermission;
import java.util.PropertyPermission;

import javax.swing.JWindow;

import net.sourceforge.jnlp.JNLPFile;
import net.sourceforge.jnlp.security.SecurityWarning.AccessType;
import net.sourceforge.jnlp.services.ServiceUtil;
import net.sourceforge.jnlp.util.WeakList;
import sun.awt.AWTSecurityManager;
import sun.awt.AppContext;
import sun.security.util.SecurityConstants;

/**
 * Security manager for JNLP environment.  This security manager
 * cannot be replaced as it always denies attempts to replace the
 * security manager or policy.<p>
 *
 * The JNLP security manager tracks windows created by an
 * application, allowing those windows to be disposed when the
 * application exits but the JVM does not.  If security is not
 * enabled then the first application to call System.exit will
 * halt the JVM.<p>
 *
 * @author <a href="mailto:jmaxwell@users.sourceforge.net">Jon A. Maxwell (JAM)</a> - initial author
 * @version $Revision: 1.17 $
 */
class JNLPSecurityManager extends AWTSecurityManager {

    // todo: some apps like JDiskReport can close the VM even when
    // an exit class is set - fix!

    // todo: create an event dispatch thread for each application,
    // so that the context classloader doesn't have to be switched
    // to the foreground application (the currently the approach
    // since some apps need their classloader as event dispatch
    // thread's context classloader).

    // todo: use a custom Permission object to identify the current
    // application in an AccessControlContext by setting a side
    // effect in its implies method.  Use a custom
    // AllPermissions-like permission to do this for apps granted
    // all permissions (but investigate whether this will nuke
    // the all-permission optimizations in the JRE).

    // todo: does not exit app if close button pressed on JFrame
    // with CLOSE_ON_EXIT (or whatever) set; if doesn't exit, use an
    // WindowListener to catch WindowClosing event, then if exit is
    // called immediately afterwards from AWT thread.

    // todo: deny all permissions to applications that should have
    // already been 'shut down' by closing their resources and
    // interrupt the threads if operating in a shared-VM (exit class
    // set).  Deny will probably will slow checks down a lot though.

    // todo: weak remember last getProperty application and
    // re-install properties if another application calls, or find
    // another way for different apps to have different properties
    // in java.lang.Sytem with the same names.

    /** only class that can exit the JVM, if set */
    private Object exitClass = null;

    /** this exception prevents exiting the JVM */
    private SecurityException closeAppEx = // making here prevents huge stack traces
        new SecurityException(R("RShutdown"));

    /** weak list of windows created */
    private WeakList<Window> weakWindows = new WeakList<Window>();

    /** weak list of applications corresponding to window list */
    private WeakList<ApplicationInstance> weakApplications =
        new WeakList<ApplicationInstance>();

    /** weak reference to most app who's windows was most recently activated */
    private WeakReference activeApplication = null;

    /** Sets whether or not exit is allowed (in the context of the plugin, this is always false) */
    private boolean exitAllowed = true;

    /**
     * The AppContext of the main application (netx). We need to store this here
     * so we can return this when no code from an external application is
     * running on the thread
     */
    private AppContext mainAppContext;

    /**
     * Creates a JNLP SecurityManager.
     */
    JNLPSecurityManager() {
        // this has the side-effect of creating the Swing shared Frame
        // owner.  Since no application is running at this time, it is
        // not added to any window list when checkTopLevelWindow is
        // called for it (and not disposed).

        if (!JNLPRuntime.isHeadless())
            new JWindow().getOwner();

        mainAppContext = AppContext.getAppContext();
    }

    /**
     * Returns whether the exit class is present on the stack, or
     * true if no exit class is set.
     */
    public boolean isExitClass() {
        return isExitClass(getClassContext());
    }

    /**
     * Returns whether the exit class is present on the stack, or
     * true if no exit class is set.
     */
    private boolean isExitClass(Class stack[]) {
        if (exitClass == null)
            return true;

        for (int i=0; i < stack.length; i++)
            if (stack[i] == exitClass)
                return true;

        return false;
    }

    /**
     * Set the exit class, which is the only class that can exit the
     * JVM; if not set then any class can exit the JVM.
     *
     * @param exitClass the exit class
     * @throws IllegalStateException if the exit class is already set
     */
    public void setExitClass(Class exitClass) throws IllegalStateException {
        if (this.exitClass != null)
            throw new IllegalStateException(R("RExitTaken"));

        this.exitClass = exitClass;
    }

    /**
     * Return the current Application, or null if none can be
     * determined.
     */
    protected ApplicationInstance getApplication() {
        return getApplication(getClassContext(), 0);
    }

    /**
     * Return the application the opened the specified window (only
     * call from event dispatch thread).
     */
    protected ApplicationInstance getApplication(Window window) {
        for (int i = weakWindows.size(); i-->0;) {
            Window w = weakWindows.get(i);
            if (w == null) {
                weakWindows.remove(i);
                weakApplications.remove(i);
            }

            if (w == window)
                return weakApplications.get(i);
        }

        return null;
    }

    /**
     * Return the current Application, or null.
     */
    protected ApplicationInstance getApplication(Class stack[], int maxDepth) {
        if (maxDepth <= 0)
                maxDepth = stack.length;

        // this needs to be tightened up
        for (int i=0; i < stack.length && i < maxDepth; i++) {
                if (stack[i].getClassLoader() instanceof JNLPClassLoader) {
                        JNLPClassLoader loader = (JNLPClassLoader) stack[i].getClassLoader();

                        if (loader != null && loader.getApplication() != null) {
                                return loader.getApplication();
                        }
                }
        }

        return null;
    }

    /**
     * Returns the application's thread group if the application can
     * be determined; otherwise returns super.getThreadGroup()
     */
    public ThreadGroup getThreadGroup() {
        ApplicationInstance app = getApplication();
        if (app == null)
            return super.getThreadGroup();

        return app.getThreadGroup();
    }

    /**
     * Throws a SecurityException if the permission is denied,
     * otherwise return normally.  This method always denies
     * permission to change the security manager or policy.
     */
    public void checkPermission(Permission perm) {
        String name = perm.getName();

        // Enable this manually -- it'll produce too much output for -verbose
        // otherwise.
        //      if (true)
        //        System.out.println("Checking permission: " + perm.toString());

        if (!JNLPRuntime.isWebstartApplication() &&
              ("setPolicy".equals(name) || "setSecurityManager".equals(name)))
            throw new SecurityException(R("RCantReplaceSM"));

        try {
            // deny all permissions to stopped applications
                // The call to getApplication() below might not work if an
                // application hasn't been fully initialized yet.
//            if (JNLPRuntime.isDebug()) {
//                if (!"getClassLoader".equals(name)) {
//                    ApplicationInstance app = getApplication();
//                    if (app != null && !app.isRunning())
//                        throw new SecurityException(R("RDenyStopped"));
//                }
//            }

                        try {
                                super.checkPermission(perm);
                        } catch (SecurityException se) {

                                //This section is a special case for dealing with SocketPermissions.
                                if (JNLPRuntime.isDebug())
                                        System.err.println("Requesting permission: " + perm.toString());

                                //Change this SocketPermission's action to connect and accept
                                //(and resolve). This is to avoid asking for connect permission
                                //on every address resolve.
                                Permission tmpPerm = null;
                                if (perm instanceof SocketPermission) {
                                        tmpPerm = new SocketPermission(perm.getName(),
                                                        SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);

                                        // before proceeding, check if we are trying to connect to same origin
                                        ApplicationInstance app = getApplication();
                                        JNLPFile file = app.getJNLPFile();

                                        String srcHost =  file.getSourceLocation().getAuthority();
                                        String destHost = name;

                                        // host = abc.xyz.com or abc.xyz.com:<port>
                                        if (destHost.indexOf(':') >= 0)
                                                destHost = destHost.substring(0, destHost.indexOf(':'));

                                        // host = abc.xyz.com
                                        String[] hostComponents = destHost.split("\\.");

                                        int length = hostComponents.length;
                                        if (length >= 2) {

                                                // address is in xxx.xxx.xxx format
                                                destHost = hostComponents[length -2] + "." + hostComponents[length -1];

                                                // host = xyz.com i.e. origin
                                                boolean isDestHostName = false;

                                                // make sure that it is not an ip address
                                                try {
                                                        Integer.parseInt(hostComponents[length -1]);
                                                } catch (NumberFormatException e) {
                                                        isDestHostName = true;
                                                }

                                                if (isDestHostName) {
                                                        // okay, destination is hostname. Now figure out if it is a subset of origin
                                                        if (srcHost.endsWith(destHost)) {
                                                                addPermission(tmpPerm);
                                                                return;
                                                        }
                                                }
                                        }

                                } else if (perm instanceof SecurityPermission) {

                                    // JCE's initialization requires putProviderProperty permission
                                    if (perm.equals(new SecurityPermission("putProviderProperty.SunJCE"))) {
                                        if (inTrustedCallChain("com.sun.crypto.provider.SunJCE", "run")) {
                                            return;
                                        }
                                    }

                                } else if (perm instanceof RuntimePermission) {

                                    // KeyGenerator's init method requires internal spec access
                                    if (perm.equals(new SecurityPermission("accessClassInPackage.sun.security.internal.spec"))) {
                                        if (inTrustedCallChain("javax.crypto.KeyGenerator", "init")) {
                                            return;
                                        }
                                    }

                                } else {
                                    tmpPerm = perm;
                                }

                                if (tmpPerm != null) {
                                    //askPermission will only prompt the user on SocketPermission
                                    //meaning we're denying all other SecurityExceptions that may arise.
                                    if (askPermission(tmpPerm)) {
                                        addPermission(tmpPerm);
                                        //return quietly.
                                    } else {
                                        throw se;
                                    }
                                }
                        }
        }
        catch (SecurityException ex) {
            if (JNLPRuntime.isDebug()) {
                System.out.println("Denying permission: "+perm);
            }
            throw ex;
        }
    }

    /**
     * Returns weather the given class and method are in the current stack,
     * and whether or not everything upto then is trusted
     *
     * @param className The name of the class to look for in the stack
     * @param methodName The name of the method for the given class to look for in the stack
     * @return Weather or not class::method() are in the chain, and everything upto there is trusted
     */
    private boolean inTrustedCallChain(String className, String methodName) {

        StackTraceElement[] stack =  Thread.currentThread().getStackTrace();

        for (int i=0; i < stack.length; i++) {

            // Everything up to the desired class/method must be trusted
            if (!stack[i].getClass().getProtectionDomain().implies(new AllPermission())) {
                return false;
            }

            if (stack[i].getClassName().equals(className) &&
                stack[i].getMethodName().equals(methodName)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Asks the user whether or not to grant permission.
     * @param perm the permission to be granted
     * @return true if the permission was granted, false otherwise.
     */
    private boolean askPermission(Permission perm)      {

        ApplicationInstance app = getApplication();
        if (app != null && !app.isSigned()) {
                if (perm instanceof SocketPermission
                                && ServiceUtil.checkAccess(AccessType.NETWORK, perm.getName())) {
                        return true;
                }
        }

        return false;
    }

    /**
     * Adds a permission to the JNLPClassLoader.
     * @param perm the permission to add to the JNLPClassLoader
     */
    private void addPermission(Permission perm) {
        if (JNLPRuntime.getApplication().getClassLoader() instanceof JNLPClassLoader) {

                JNLPClassLoader cl = (JNLPClassLoader) JNLPRuntime.getApplication().getClassLoader();
                cl.addPermission(perm);
                if (JNLPRuntime.isDebug()) {
                        if (cl.getPermissions(null).implies(perm))
                                System.err.println("Added permission: " + perm.toString());
                        else
                                System.err.println("Unable to add permission: " + perm.toString());
                }
        } else {
                if (JNLPRuntime.isDebug())
                        System.err.println("Unable to add permission: " + perm + ", classloader not JNLP.");
        }
    }

    /**
     * Checks whether the window can be displayed without an applet
     * warning banner, and adds the window to the list of windows to
     * be disposed when the calling application exits.
     */
    public boolean checkTopLevelWindow(Object window) {
        ApplicationInstance app = getApplication();

        // remember window -> application mapping for focus, close on exit
        if (app != null && window instanceof Window) {
            Window w = (Window) window;

            if (JNLPRuntime.isDebug())
                System.err.println("SM: app: "+app.getTitle()+" is adding a window: "+window+" with appContext "+AppContext.getAppContext());

            weakWindows.add(w); // for mapping window -> app
            weakApplications.add(app);

            app.addWindow(w);
        }

        // change coffee cup to netx for default icon
        if (window instanceof Window)
            for (Window w = (Window)window; w != null; w = w.getOwner())
                if (window instanceof Frame)
                    ((Frame)window).setIconImage(JNLPRuntime.getWindowIcon());

        // todo: set awt.appletWarning to custom message
        // todo: logo on with glass pane on JFrame/JWindow?

        return super.checkTopLevelWindow(window);
    }

    /**
     * Checks whether the caller can exit the system.  This method
     * identifies whether the caller is a real call to Runtime.exec
     * and has special behavior when returning from this method
     * would exit the JVM and an exit class is set: if the caller is
     * not the exit class then the calling application will be
     * stopped and its resources destroyed (when possible), and an
     * exception will be thrown to prevent the JVM from shutting
     * down.<p>
     *
     * Calls not from Runtime.exit or with no exit class set will
     * behave normally, and the exit class can always exit the JVM.
     */
    public void checkExit(int status) {

        // applets are not allowed to exit, but the plugin main class (primordial loader) is
        Class stack[] = getClassContext();
        if (!exitAllowed) {
                for (int i=0; i < stack.length; i++)
                        if (stack[i].getClassLoader() != null)
                                throw new AccessControlException("Applets may not call System.exit()");
        }

        super.checkExit(status);

        boolean realCall = (stack[1] == Runtime.class);

        if (isExitClass(stack)) // either exitClass called or no exitClass set
            return; // to Runtime.exit or fake call to see if app has permission

        // not called from Runtime.exit()
        if (!realCall) {
            // apps that can't exit should think they can exit normally
            super.checkExit(status);
            return;
        }

        // but when they really call, stop only the app instead of the JVM
        ApplicationInstance app = getApplication(stack, 0);
        if (app == null) {
            // should check caller to make sure it is JFrame.close or
            // other known System.exit call
            if (activeApplication != null)
                app = (ApplicationInstance) activeApplication.get();

            if (app == null)
                throw new SecurityException(R("RExitNoApp"));
        }

        app.destroy();

        throw closeAppEx;
    }

    protected void disableExit() {
        exitAllowed = false;
    }

    /**
     * This returns the appropriate {@link AppContext}. Hooks in AppContext
     * check if the current {@link SecurityManager} is an instance of
     * AWTSecurityManager and if so, call this method to give it a chance to
     * return the appropriate appContext based on the application that is
     * running.<p>
     *
     * This can be called from any thread (possibly a swing thread) to find out
     * the AppContext for the thread (which may correspond to a particular
     * applet).
     */
    @Override
    public AppContext getAppContext() {
        ApplicationInstance app = getApplication();
        if (app == null) {
            /*
             * if we cannot find an application based on the code on the stack,
             * then assume it is the main application
             */
            return mainAppContext;
        } else {
            return app.getAppContext();
        }

    }

    /**
     * Tests if a client can get access to the AWT event queue. This version allows
     * complete access to the EventQueue for its own AppContext-specific EventQueue.
     *
     * FIXME there are probably huge security implications for this. Eg:
     * http://hg.openjdk.java.net/jdk7/awt/jdk/rev/8022709a306d
     *
     * @exception  SecurityException  if the caller does not have
     *             permission to accesss the AWT event queue.
     */
    public void checkAwtEventQueueAccess() {
        /*
         * this is the templace of the code that should allow applets access to
         * eventqueues
         */

        // AppContext appContext = AppContext.getAppContext();
        // ApplicationInstance instance = getApplication();

        // if ((appContext == mainAppContext) && (instance != null)) {
        // If we're about to allow access to the main EventQueue,
        // and anything untrusted is on the class context stack,
        // disallow access.
        super.checkAwtEventQueueAccess();
        // }
    }

}