1 /*
2 Copyright (C) 2011 Red Hat, Inc.
3 
4 This file is part of IcedTea.
5 
6 IcedTea is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, version 2.
9 
10 IcedTea is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 General Public License for more details.
14 
15 You should have received a copy of the GNU General Public License
16 along with IcedTea; see the file COPYING.  If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18 02110-1301 USA.
19 
20 Linking this library statically or dynamically with other modules is
21 making a combined work based on this library.  Thus, the terms and
22 conditions of the GNU General Public License cover the whole
23 combination.
24 
25 As a special exception, the copyright holders of this library give you
26 permission to link this library with independent modules to produce an
27 executable, regardless of the license terms of these independent
28 modules, and to copy and distribute the resulting executable under
29 terms of your choice, provided that you also meet, for each linked
30 independent module, the terms and conditions of the license of that
31 module.  An independent module is a module which is not derived from
32 or based on this library.  If you modify this library, you may extend
33 this exception to your version of the library, but you are not
34 obligated to do so.  If you do not wish to do so, delete this
35 exception statement from your version.
36  */
37 package net.sourceforge.jnlp.runtime;
38 
39 import java.net.MalformedURLException;
40 import java.net.URL;
41 import java.util.ArrayList;
42 import java.util.HashSet;
43 import java.util.List;
44 import java.util.Set;
45 
46 import net.sourceforge.jnlp.ExtensionDesc;
47 import net.sourceforge.jnlp.JARDesc;
48 import net.sourceforge.jnlp.JNLPFile;
49 import net.sourceforge.jnlp.JNLPFile.ManifestBoolean;
50 import net.sourceforge.jnlp.SecurityDesc.RequestedPermissionLevel;
51 import net.sourceforge.jnlp.LaunchException;
52 import net.sourceforge.jnlp.PluginBridge;
53 import net.sourceforge.jnlp.ResourcesDesc;
54 import net.sourceforge.jnlp.SecurityDesc;
55 import net.sourceforge.jnlp.config.DeploymentConfiguration;
56 import net.sourceforge.jnlp.runtime.JNLPClassLoader.SecurityDelegate;
57 import net.sourceforge.jnlp.runtime.JNLPClassLoader.SigningState;
58 import net.sourceforge.jnlp.security.SecurityDialogs;
59 import net.sourceforge.jnlp.security.appletextendedsecurity.AppletSecurityLevel;
60 import net.sourceforge.jnlp.security.appletextendedsecurity.AppletStartupSecuritySettings;
61 import net.sourceforge.jnlp.util.ClasspathMatcher.ClasspathMatchers;
62 import net.sourceforge.jnlp.util.UrlUtils;
63 import net.sourceforge.jnlp.util.logging.OutputController;
64 
65 import static net.sourceforge.jnlp.config.BasicValueValidators.splitCombination;
66 import static net.sourceforge.jnlp.runtime.Translator.R;
67 
68 public class ManifestAttributesChecker {
69 
70     private final SecurityDesc security;
71     private final JNLPFile file;
72     private final SigningState signing;
73     private final SecurityDelegate securityDelegate;
74 
ManifestAttributesChecker(final SecurityDesc security, final JNLPFile file, final SigningState signing, final SecurityDelegate securityDelegate)75     public ManifestAttributesChecker(final SecurityDesc security, final JNLPFile file,
76             final SigningState signing, final SecurityDelegate securityDelegate) throws LaunchException {
77         this.security = security;
78         this.file = file;
79         this.signing = signing;
80         this.securityDelegate = securityDelegate;
81     }
82 
83     public enum MANIFEST_ATTRIBUTES_CHECK {
84         ALL,
85         NONE,
86         PERMISSIONS,
87         CODEBASE,
88         TRUSTED,
89         ALAC,
90         ENTRYPOINT
91     }
92 
checkAll()93     void checkAll() throws LaunchException {
94         List<MANIFEST_ATTRIBUTES_CHECK> attributesCheck = getAttributesCheck();
95         if (attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.NONE)) {
96             OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("MACDisabledMessage"));
97         } else {
98 
99             if (attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.TRUSTED) ||
100                     attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.ALL)) {
101                 checkTrustedOnlyAttribute();
102             } else {
103                 OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("MACCheckSkipped", "Trusted-Only", "TRUSTED"));
104             }
105 
106             if (attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.CODEBASE) ||
107                     attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.ALL)) {
108                 checkCodebaseAttribute();
109             } else {
110                 OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("MACCheckSkipped", "Codebase", "CODEBASE"));
111             }
112 
113             if (attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.PERMISSIONS) ||
114                     attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.ALL)) {
115                 checkPermissionsAttribute();
116             } else {
117                 OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("MACCheckSkipped", "Permissions", "PERMISSIONS"));
118             }
119 
120             if (attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.ALAC) ||
121                    attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.ALL)) {
122                 checkApplicationLibraryAllowableCodebaseAttribute();
123             } else {
124                 OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("MACCheckSkipped", "Application Library Allowable Codebase", "ALAC"));
125             }
126 
127             if (attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.ENTRYPOINT)
128                     || attributesCheck.contains(MANIFEST_ATTRIBUTES_CHECK.ALL)) {
129                 checkEntryPoint();
130             } else {
131                 OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("MACCheckSkipped", "Entry-Point", "ENTRYPOINT"));
132             }
133 
134         }
135     }
136 
getAttributesCheck()137     public static List<MANIFEST_ATTRIBUTES_CHECK> getAttributesCheck() {
138         final String deploymentProperty = JNLPRuntime.getConfiguration().getProperty(DeploymentConfiguration.KEY_ENABLE_MANIFEST_ATTRIBUTES_CHECK);
139         String[] attributesCheck = splitCombination(deploymentProperty);
140         List<MANIFEST_ATTRIBUTES_CHECK> manifestAttributesCheckList = new ArrayList<>();
141         for (String attribute : attributesCheck) {
142             for (MANIFEST_ATTRIBUTES_CHECK manifestAttribute  : MANIFEST_ATTRIBUTES_CHECK.values()) {
143                 if (manifestAttribute.toString().equals(attribute)) {
144                     manifestAttributesCheckList.add(manifestAttribute);
145                 }
146             }
147         }
148         return manifestAttributesCheckList;
149     }
150     /*
151      * http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/security/manifest.html#entry_pt
152      */
checkEntryPoint()153     private void checkEntryPoint() throws LaunchException {
154         if (signing == SigningState.NONE) {
155             return; /*when app is not signed at all, then skip this check*/
156         }
157         if (file.getLaunchInfo() == null) {
158             OutputController.getLogger().log(OutputController.Level.MESSAGE_DEBUG, "Entry-Point can not be checked now, because of not existing launch info.");
159             return;
160         }
161         if (file.getLaunchInfo().getMainClass() == null) {
162             OutputController.getLogger().log(OutputController.Level.MESSAGE_DEBUG, "Entry-Point can not be checked now, because of unknown main class.");
163             return;
164         }
165         final String[] eps = file.getManifestsAttributes().getEntryPoints();
166         String mainClass = file.getLaunchInfo().getMainClass();
167         if (eps == null) {
168             OutputController.getLogger().log(OutputController.Level.MESSAGE_DEBUG, "Entry-Point manifest attribute for yours '" + mainClass + "'not found. Continuing.");
169             return;
170         }
171         for (String ep : eps) {
172             if (ep.equals(mainClass)) {
173                 OutputController.getLogger().log(OutputController.Level.MESSAGE_DEBUG, "Entry-Point of " + ep + " mathches " + mainClass + " continuing.");
174                 return;
175             }
176         }
177         throw new LaunchException("None of the entry points specified: '" + file.getManifestsAttributes().getEntryPointString() + "' matched the main class " + mainClass + " and apelt is signed. This is a security error and the app will not be launched.");
178     }
179 
180     /**
181      * http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/security/manifest.html#trusted_only
182      */
checkTrustedOnlyAttribute()183     private void checkTrustedOnlyAttribute() throws LaunchException {
184         final ManifestBoolean trustedOnly = file.getManifestsAttributes().isTrustedOnly();
185         if (trustedOnly == ManifestBoolean.UNDEFINED) {
186             OutputController.getLogger().log(OutputController.Level.MESSAGE_DEBUG, "Trusted Only manifest attribute not found. Continuing.");
187             return;
188         }
189 
190         if (trustedOnly == ManifestBoolean.FALSE) {
191             OutputController.getLogger().log(OutputController.Level.MESSAGE_DEBUG, "Trusted Only manifest attribute is false. Continuing.");
192             return;
193         }
194 
195         final Object desc = security.getSecurityType();
196 
197         final String securityType;
198         if (desc == null) {
199             securityType = "Not Specified";
200         } else if (desc.equals(SecurityDesc.ALL_PERMISSIONS)) {
201             securityType = "All-Permission";
202         } else if (desc.equals(SecurityDesc.SANDBOX_PERMISSIONS)) {
203             securityType = "Sandbox";
204         } else if (desc.equals(SecurityDesc.J2EE_PERMISSIONS)) {
205             securityType = "J2EE";
206         } else {
207             securityType = "Unknown";
208         }
209 
210         final boolean isFullySigned = signing == SigningState.FULL;
211         final boolean isSandboxed = securityDelegate.getRunInSandbox();
212         final boolean requestsCorrectPermissions = (isFullySigned && SecurityDesc.ALL_PERMISSIONS.equals(desc))
213                 || (isSandboxed && SecurityDesc.SANDBOX_PERMISSIONS.equals(desc));
214         final String signedMsg;
215         if (isFullySigned && !isSandboxed) {
216             signedMsg = R("STOAsignedMsgFully");
217         } else if (isFullySigned && isSandboxed) {
218             signedMsg = R("STOAsignedMsgAndSandbox");
219         } else {
220             signedMsg = R("STOAsignedMsgPartiall");
221         }
222         OutputController.getLogger().log(OutputController.Level.MESSAGE_DEBUG,
223                 "Trusted Only manifest attribute is \"true\". " + signedMsg + " and requests permission level: " + securityType);
224         if (!(isFullySigned && requestsCorrectPermissions)) {
225             throw new LaunchException(R("STrustedOnlyAttributeFailure", signedMsg, securityType));
226         }
227     }
228 
229     /**
230      * http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/manifest.html#codebase
231      */
checkCodebaseAttribute()232     private void checkCodebaseAttribute() throws LaunchException {
233         if (file.getCodeBase() == null || file.getCodeBase().getProtocol().equals("file")) {
234             OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("CBCheckFile"));
235             return;
236         }
237         final Object securityType = security.getSecurityType();
238         final URL codebase = UrlUtils.guessCodeBase(file);
239         final ClasspathMatchers codebaseAtt = file.getManifestsAttributes().getCodebase();
240         if (codebaseAtt == null) {
241             OutputController.getLogger().log(OutputController.Level.WARNING_ALL, R("CBCheckNoEntry"));
242             return;
243         }
244         if (securityType.equals(SecurityDesc.SANDBOX_PERMISSIONS)) {
245             if (codebaseAtt.matches(codebase)) {
246                 OutputController.getLogger().log(OutputController.Level.MESSAGE_ALL, R("CBCheckUnsignedPass"));
247             } else {
248                 OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("CBCheckUnsignedFail"));
249             }
250         } else {
251             if (codebaseAtt.matches(codebase)) {
252                 OutputController.getLogger().log(OutputController.Level.MESSAGE_ALL, R("CBCheckOkSignedOk"));
253             } else {
254                 if (file instanceof PluginBridge) {
255                     throw new LaunchException(R("CBCheckSignedAppletDontMatchException", file.getManifestsAttributes().getCodebase().toString(), codebase));
256                 } else {
257                     OutputController.getLogger().log(OutputController.Level.ERROR_ALL, R("CBCheckSignedFail"));
258                 }
259             }
260         }
261 
262     }
263 
264     /**
265      * http://docs.oracle.com/javase/7/docs/technotes/guides/jweb/security/manifest.html#permissions
266      */
checkPermissionsAttribute()267     private void checkPermissionsAttribute() throws LaunchException {
268         if (securityDelegate.getRunInSandbox()) {
269             OutputController.getLogger().log(OutputController.Level.WARNING_ALL, "The 'Permissions' attribute of this application is '" + file.getManifestsAttributes().permissionsToString()
270                     + "'. You have chosen the Sandbox run option, which overrides the Permissions manifest attribute, or the applet has already been automatically sandboxed.");
271             return;
272         }
273 
274         final ManifestBoolean sandboxForced = file.getManifestsAttributes().isSandboxForced();
275         // If the attribute is not specified in the manifest, prompt the user. Oracle's spec says that the
276         // attribute is required, but this breaks a lot of existing applets. Therefore, when on the highest
277         // security level, we refuse to run these applets. On the standard security level, we ask. And on the
278         // lowest security level, we simply proceed without asking.
279         if (sandboxForced == ManifestBoolean.UNDEFINED) {
280             final AppletSecurityLevel itwSecurityLevel = AppletStartupSecuritySettings.getInstance().getSecurityLevel();
281             if (itwSecurityLevel == AppletSecurityLevel.DENY_UNSIGNED) {
282                 throw new LaunchException("Your Extended applets security is at 'Very high', and this application is missing the 'permissions' attribute in manifest. This is fatal");
283             }
284             if (itwSecurityLevel == AppletSecurityLevel.ASK_UNSIGNED) {
285                 final boolean userApproved = SecurityDialogs.showMissingPermissionsAttributeDialogue(file.getTitle(), file.getNotNullProbalbeCodeBase().toExternalForm());
286                 if (!userApproved) {
287                     throw new LaunchException("Your Extended applets security is at 'high' and this application is missing the 'permissions' attribute in manifest. And you have refused to run it.");
288                 } else {
289                     OutputController.getLogger().log("Your Extended applets security is at 'high' and this application is missing the 'permissions' attribute in manifest. And you have allowed to run it.");
290                 }
291             }
292             return;
293         }
294 
295         final RequestedPermissionLevel requestedPermissions = file.getRequestedPermissionLevel();
296         validateRequestedPermissionLevelMatchesManifestPermissions(requestedPermissions, sandboxForced);
297         if (file instanceof PluginBridge) { // HTML applet
298             if (isNoneOrDefault(requestedPermissions)) {
299                 if (sandboxForced == ManifestBoolean.TRUE && signing != SigningState.NONE) {
300                     securityDelegate.setRunInSandbox();
301                 }
302             }
303         } else { // JNLP
304             if (isNoneOrDefault(requestedPermissions)) {
305                 if (sandboxForced == ManifestBoolean.TRUE && signing != SigningState.NONE) {
306                     OutputController.getLogger().log(OutputController.Level.WARNING_ALL, "The 'permissions' attribute is '" + file.getManifestsAttributes().permissionsToString() + "' and the applet is signed. Forcing sandbox.");
307                     securityDelegate.setRunInSandbox();
308                 }
309                 if (sandboxForced == ManifestBoolean.FALSE && signing == SigningState.NONE) {
310                     OutputController.getLogger().log(OutputController.Level.WARNING_ALL, "The 'permissions' attribute is '" + file.getManifestsAttributes().permissionsToString() + "' and the applet is unsigned. Forcing sandbox.");
311                     securityDelegate.setRunInSandbox();
312                 }
313             }
314         }
315     }
316 
isLowSecurity()317     private static boolean isLowSecurity() {
318         return AppletStartupSecuritySettings.getInstance().getSecurityLevel().equals(AppletSecurityLevel.ALLOW_UNSIGNED);
319     }
320 
isNoneOrDefault(final RequestedPermissionLevel requested)321     private static boolean isNoneOrDefault(final RequestedPermissionLevel requested) {
322         return requested == RequestedPermissionLevel.NONE || requested == RequestedPermissionLevel.DEFAULT;
323     }
324 
validateRequestedPermissionLevelMatchesManifestPermissions(final RequestedPermissionLevel requested, final ManifestBoolean sandboxForced)325     private void validateRequestedPermissionLevelMatchesManifestPermissions(final RequestedPermissionLevel requested, final ManifestBoolean sandboxForced) throws LaunchException {
326         if (requested == RequestedPermissionLevel.ALL && sandboxForced != ManifestBoolean.FALSE) {
327             throw new LaunchException("The 'permissions' attribute is '" + file.getManifestsAttributes().permissionsToString() + "' but the applet requested " + requested + ". This is fatal");
328         }
329 
330         if (requested == RequestedPermissionLevel.SANDBOX && sandboxForced != ManifestBoolean.TRUE) {
331             throw new LaunchException("The 'permissions' attribute is '" + file.getManifestsAttributes().permissionsToString() + "' but the applet requested " + requested + ". This is fatal");
332         }
333     }
334 
checkApplicationLibraryAllowableCodebaseAttribute()335     private void checkApplicationLibraryAllowableCodebaseAttribute() throws LaunchException {
336         //conditions
337         URL codebase = file.getCodeBase();
338         URL documentBase = null;
339         if (file instanceof PluginBridge) {
340             documentBase = ((PluginBridge) file).getSourceLocation();
341         }
342         if (documentBase == null) {
343             documentBase = file.getCodeBase();
344         }
345 
346         //cases
347         Set<URL> usedUrls = new HashSet<URL>();
348         URL sourceLocation = file.getSourceLocation();
349         ResourcesDesc[] resourcesDescs = file.getResourcesDescs();
350         if (sourceLocation != null) {
351             usedUrls.add(UrlUtils.removeFileName(sourceLocation));
352         }
353         for (ResourcesDesc resourcesDesc : resourcesDescs) {
354             ExtensionDesc[] ex = resourcesDesc.getExtensions();
355             if (ex != null) {
356                 for (ExtensionDesc extensionDesc : ex) {
357                     if (extensionDesc != null) {
358                         usedUrls.add(UrlUtils.removeFileName(extensionDesc.getLocation()));
359                     }
360                 }
361             }
362             JARDesc[] jars = resourcesDesc.getJARs();
363             if (jars != null) {
364                 for (JARDesc jarDesc : jars) {
365                     if (jarDesc != null) {
366                         usedUrls.add(UrlUtils.removeFileName(jarDesc.getLocation()));
367                     }
368                 }
369             }
370             JNLPFile jnlp = resourcesDesc.getJNLPFile();
371             if (jnlp != null) {
372                 usedUrls.add(UrlUtils.removeFileName(jnlp.getSourceLocation()));
373             }
374 
375         }
376         OutputController.getLogger().log("Found alaca URLs to be verified");
377         for (URL url : usedUrls) {
378             OutputController.getLogger().log(" - " + url.toExternalForm());
379         }
380         if (usedUrls.isEmpty()) {
381             //I hope this is the case, when the resources is/are
382             //only codebase classes. Then it should be safe to return.
383             OutputController.getLogger().log("The application is not using any url resources, skipping Application-Library-Allowable-Codebase Attribute check.");
384             return;
385         }
386 
387         boolean allOk = true;
388         for (URL u : usedUrls) {
389             if (UrlUtils.equalsIgnoreLastSlash(u, codebase)
390                     && UrlUtils.equalsIgnoreLastSlash(u, stripDocbase(documentBase))) {
391                 OutputController.getLogger().log("OK - "+u.toExternalForm()+" is from codebase/docbase.");
392             } else {
393                 allOk = false;
394                 OutputController.getLogger().log("Warning! "+u.toExternalForm()+" is NOT from codebase/docbase.");
395             }
396         }
397         if (allOk) {
398             //all resoources are from codebase or document base. it is ok to proceeed.
399             OutputController.getLogger().log("All applications resources (" + usedUrls.toArray(new URL[0])[0] + ") are from codebas/documentbase " + codebase + "/" + documentBase + ", skipping Application-Library-Allowable-Codebase Attribute check.");
400             return;
401         }
402 
403         ClasspathMatchers att = null;
404         if (signing == SigningState.NONE) {
405             //for unsigned app we are ignoring value in manifesdt (may be faked)
406         } else {
407             att = file.getManifestsAttributes().getApplicationLibraryAllowableCodebase();
408         }
409         if (att == null) {
410             final boolean userApproved = SecurityDialogs.showMissingALACAttributePanel(file.getTitle(), file.getNotNullProbalbeCodeBase(), usedUrls);
411             if (!userApproved) {
412                 throw new LaunchException("The application uses non-codebase resources, has no Application-Library-Allowable-Codebase Attribute, and was blocked from running by the user");
413             } else {
414                 OutputController.getLogger().log("The application uses non-codebase resources, has no Application-Library-Allowable-Codebase Attribute, and was allowed to run by the user or user's security settings");
415                 return;
416             }
417         } else {
418             for (URL foundUrl : usedUrls) {
419                 if (!att.matches(foundUrl)) {
420                     throw new LaunchException("The resource from " + foundUrl + " does not match the  location in Application-Library-Allowable-Codebase Attribute " + att + ". Blocking the application from running.");
421                 } else {
422                     OutputController.getLogger().log("The resource from " + foundUrl + " does  match the  location in Application-Library-Allowable-Codebase Attribute " + att + ". Continuing.");
423                 }
424             }
425         }
426         final boolean userApproved = isLowSecurity() || SecurityDialogs.showMatchingALACAttributePanel(file, documentBase, usedUrls);
427         if (!userApproved) {
428             throw new LaunchException("The application uses non-codebase resources, which do match its Application-Library-Allowable-Codebase Attribute, but was blocked from running by the user.");
429         } else {
430             OutputController.getLogger().log("The application uses non-codebase resources, which do match its Application-Library-Allowable-Codebase Attribute, and was allowed to run by the user or user's security settings.");
431         }
432     }
433 
434     //package private for testing
435     //not perfect but ok for usecase
stripDocbase(URL documentBase)436     static URL stripDocbase(URL documentBase) {
437         String s = documentBase.toExternalForm();
438         if (s.endsWith("/") || s.endsWith("\\")) {
439             return documentBase;
440         }
441         int i1 = s.lastIndexOf("/");
442         int i2 = s.lastIndexOf("\\");
443         int i = Math.max(i1, i2);
444         if (i <= 8 || i >= s.length()) {
445             return documentBase;
446         }
447         s = s.substring(0, i+1);
448         try {
449             documentBase = new URL(s);
450         } catch (MalformedURLException ex) {
451             OutputController.getLogger().log(ex);
452         }
453         return documentBase;
454     }
455 }
456