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