diff --git a/.ci/build-pr-win-macos.sh b/.ci/build-pr-win-macos.sh index a6dca3172b..6fadee2b26 100755 --- a/.ci/build-pr-win-macos.sh +++ b/.ci/build-pr-win-macos.sh @@ -24,7 +24,7 @@ log_group_start "Downloading maven dependencies" log_group_end log_group_start "Building with maven" - ./mvnw -e -V clean verify ${PMD_EXTRA_OPT} + ./mvnw -e -V -B clean verify ${PMD_EXTRA_OPT} log_group_end diff --git a/.ci/build.sh b/.ci/build.sh index f7651bff23..6dcecba98f 100755 --- a/.ci/build.sh +++ b/.ci/build.sh @@ -121,7 +121,7 @@ function pmd_ci_build_run() { log_info "This is a snapshot build" fi - ./mvnw clean deploy -P${mvn_profiles} -e -V -Djava7.home=${HOME}/oraclejdk7 + ./mvnw clean deploy -P${mvn_profiles} -e -B -V -Djava7.home=${HOME}/oraclejdk7 } # diff --git a/.ci/files/project-list.xml b/.ci/files/project-list.xml index d150ae3443..9117207eaa 100644 --- a/.ci/files/project-list.xml +++ b/.ci/files/project-list.xml @@ -32,6 +32,8 @@ mvn dependency:build-classpath -DincludeScope=test -Dmdep.outputFile=classpath.t https://github.com/spring-projects/spring-framework v5.0.6.RELEASE + .*/build/generated-sources/.* + classpath.txt ]]> cat classpath.txt diff --git a/.ci/inc/maven-dependencies.inc b/.ci/inc/maven-dependencies.inc index c99279b395..7e6105ce88 100644 --- a/.ci/inc/maven-dependencies.inc +++ b/.ci/inc/maven-dependencies.inc @@ -23,19 +23,20 @@ function maven_dependencies_resolve() { # build first the modules, that have dependencies between themselves # first build pmd-lang-test, pmd-test and pmd-core - used by all modules ./mvnw clean install -pl pmd-core,pmd-test,pmd-lang-test -DskipTests -Dpmd.skip=true \ - -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true + -B -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true # then build dependencies for pmd-visualforce needs: pmd-apex->pmd-apex-jorje+pmd-test+pmd-core ./mvnw clean install -pl pmd-core,pmd-test,pmd-lang-test,pmd-apex-jorje,pmd-apex -DskipTests -Dpmd.skip=true \ - -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true + -B -Dcheckstyle.skip=true -Dmaven.javadoc.skip=true -Dmaven.source.skip=true # the resolve most other projects. The excluded projects depend on other projects in the reactor, which is not # completely built yet, so these are excluded. - ./mvnw dependency:resolve -pl '!pmd-dist,!pmd-java8,!pmd-doc,!pmd-scala' + ./mvnw dependency:resolve -pl '!pmd-dist,!pmd-java8,!pmd-doc,!pmd-scala' -Dsilent -B - ./mvnw dependency:get -DgroupId=org.jetbrains.dokka \ + ./mvnw dependency:get -B -Dsilent \ + -DgroupId=org.jetbrains.dokka \ -DartifactId=dokka-maven-plugin \ -Dversion=${dokka_version} \ -Dpackaging=jar \ -DremoteRepositories=jcenter::default::https://jcenter.bintray.com/ - ./mvnw dependency:resolve-plugins -DexcludeGroupIds=org.jetbrains.dokka -Psign + ./mvnw dependency:resolve-plugins -B -Dsilent -DexcludeGroupIds=org.jetbrains.dokka -Psign } diff --git a/.ci/inc/regression-tester.inc b/.ci/inc/regression-tester.inc index e959e9a780..3bc72ccd81 100644 --- a/.ci/inc/regression-tester.inc +++ b/.ci/inc/regression-tester.inc @@ -33,95 +33,55 @@ function regression_tester_setup_ci() { } # -# Generate a new baseline and upload it to sourceforge -# -# Note: this function always succeeds, even if the upload fails. -# In that case, just a error logging is provided. +# Generate a new baseline and upload it to pmd-code.org # function regression_tester_uploadBaseline() { - log_debug "$FUNCNAME branch=${PMD_CI_BRANCH}" - local targetUrl="https://sourceforge.net/projects/pmd/files/pmd-regression-tester/" local pmdcodeUrl="https://pmd-code.org/pmd-regression-tester/" + local baseline_branch="${PMD_CI_BRANCH:-$PMD_CI_TAG}" + log_debug "$FUNCNAME branch=${baseline_branch}" - local errexitstate="$(shopt -po errexit)" - set +e # disable errexit - ( - # This handler is called if any command fails - function upload_failed() { - log_error "Error while uploading ${BRANCH_FILENAME}-baseline.zip to pmd-code.org!" - log_error "Please upload manually: ${pmdcodeUrl}" - #log_error "Error while uploading ${BRANCH_FILENAME}-baseline.zip to sourceforge!" - #log_error "Please upload manually: ${targetUrl}" - } - - # exit subshell after trap - set -e - trap upload_failed ERR - - log_info "Generating and uploading baseline for pmdtester..." - cd .. - bundle config --local gemfile pmd/Gemfile - bundle config set --local path pmd/vendor/bundle - bundle exec pmdtester \ - --mode single \ - --local-git-repo ./pmd \ - --patch-branch ${PMD_CI_BRANCH:-$PMD_CI_TAG} \ - --patch-config ./pmd/.ci/files/all-java.xml \ - --list-of-project ./pmd/.ci/files/project-list.xml --html-flag \ - --error-recovery - cd target/reports - BRANCH_FILENAME="${PMD_CI_BRANCH:-$PMD_CI_TAG}" - BRANCH_FILENAME="${BRANCH_FILENAME/\//_}" - zip -q -r ${BRANCH_FILENAME}-baseline.zip ${BRANCH_FILENAME}/ - # ssh-key for pmd-code.org is setup already by pmd_ci_setup_ssh - scp ${BRANCH_FILENAME}-baseline.zip pmd@pmd-code.org:/httpdocs/pmd-regression-tester/ - log_success "Successfully uploaded ${BRANCH_FILENAME}-baseline.zip to ${pmdcodeUrl}" - #../../pmd/.ci/travis_wait "rsync -avh ${BRANCH_FILENAME}-baseline.zip ${PMD_SF_USER}@web.sourceforge.net:/home/frs/project/pmd/pmd-regression-tester/" - #log_success "Successfully uploaded ${BRANCH_FILENAME}-baseline.zip to ${targetUrl}" - ) - # restore errexit state - eval "$errexitstate" + log_info "Generating and uploading baseline for pmdtester (${baseline_branch})..." + pushd .. + rm -f .bundle/config + bundle config set --local gemfile pmd/Gemfile + bundle exec pmdtester \ + --mode single \ + --local-git-repo ./pmd \ + --patch-branch ${baseline_branch} \ + --patch-config ./pmd/.ci/files/all-java.xml \ + --list-of-project ./pmd/.ci/files/project-list.xml --html-flag \ + --error-recovery + pushd target/reports + BRANCH_FILENAME="${baseline_branch/\//_}" + zip -q -r ${BRANCH_FILENAME}-baseline.zip ${BRANCH_FILENAME}/ + # ssh-key for pmd-code.org is setup already by pmd_ci_setup_ssh + scp ${BRANCH_FILENAME}-baseline.zip pmd@pmd-code.org:/httpdocs/pmd-regression-tester/ + log_success "Successfully uploaded ${BRANCH_FILENAME}-baseline.zip to ${pmdcodeUrl}" + popd + popd } # # Execute danger, which executes pmd-regression-tester (via Dangerfile). # -# Note: this function always succeeds, even if the danger fails. -# In that case, just a error logging is provided. -# function regression_tester_executeDanger() { log_debug "$FUNCNAME" - local errexitstate="$(shopt -po errexit)" - set +e # disable errexit - ( - # This handler is called if any command fails - function danger_failed() { - log_error "Error while executing danger/pmd-regression-tester" - } + # Create a corresponding remote branch locally + if ! git show-ref --verify --quiet refs/heads/${PMD_CI_BRANCH}; then + git fetch --no-tags --depth=1 origin +refs/heads/${PMD_CI_BRANCH}:refs/remotes/origin/${PMD_CI_BRANCH} + git branch ${PMD_CI_BRANCH} origin/${PMD_CI_BRANCH} + log_debug "Created local branch ${PMD_CI_BRANCH}" + fi + # Fetch more commits of the PR for danger and regression tester + git fetch --no-tags --depth=50 origin +$(git rev-parse HEAD^2): + # Fetch more commits from master branch for regression tester + if [[ "${PMD_CI_BRANCH}" != "master" ]]; then + git fetch --no-tags --depth=50 origin +master: + git branch master origin/master + fi - # exit subshell after trap - set -e - trap danger_failed ERR - - # Create a corresponding remote branch locally - if ! git show-ref --verify --quiet refs/heads/${PMD_CI_BRANCH}; then - git fetch --no-tags --depth=1 origin +refs/heads/${PMD_CI_BRANCH}:refs/remotes/origin/${PMD_CI_BRANCH} - git branch ${PMD_CI_BRANCH} origin/${PMD_CI_BRANCH} - log_debug "Created local branch ${PMD_CI_BRANCH}" - fi - # Fetch more commits of the PR for danger and regression tester - git fetch --no-tags --depth=50 origin +$(git rev-parse HEAD^2): - # Fetch more commits from master branch for regression tester - if [[ "${PMD_CI_BRANCH}" != "master" ]]; then - git fetch --no-tags --depth=50 origin +master: - git branch master origin/master - fi - - log_info "Running danger on branch ${PMD_CI_BRANCH}" - bundle exec danger --verbose - log_success "Executing danger successfully" - ) - # restore errexit state - eval "$errexitstate" + log_info "Running danger on branch ${PMD_CI_BRANCH}" + bundle exec danger --verbose + log_success "Executed danger successfully" } diff --git a/.github/ISSUE_TEMPLATE/rule_violation.md b/.github/ISSUE_TEMPLATE/rule_violation.md index dd2c61ab0d..0b5636533b 100644 --- a/.github/ISSUE_TEMPLATE/rule_violation.md +++ b/.github/ISSUE_TEMPLATE/rule_violation.md @@ -2,7 +2,7 @@ name: Rule violation about: Let us know about a false positive/false negative title: '' -labels: 'a:bug' +labels: 'a:false-positive, a:false-negative' assignees: '' --- diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 169f000a80..9fd0c2fcbe 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -6,7 +6,7 @@ jobs: build: runs-on: ${{ matrix.os }} continue-on-error: false - timeout-minutes: 30 + timeout-minutes: 60 strategy: matrix: os: [ ubuntu-latest, windows-latest, macos-latest ] diff --git a/.gitignore b/.gitignore index 3c33657fab..51a3ccab88 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ bin/ pmd-core/dependency-reduced-pom.xml .bundle vendor +.DS_Store # rule docs are generated docs/pages/pmd/rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 372eab4e2d..0172b4acc9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,7 @@ There are various channels, on which you can ask questions: * On [StackOverflow](https://stackoverflow.com/questions/tagged/pmd): Make sure, to tag your question with "pmd". -* Create a issue for your question at . +* Create a new discussion for your question at . * Ask your question on Gitter . diff --git a/Dangerfile b/Dangerfile index 07cc07a715..1d51856dab 100644 --- a/Dangerfile +++ b/Dangerfile @@ -16,7 +16,7 @@ def run_pmdtester '--auto-gen-config', '--error-recovery', '--baseline-download-url', 'https://pmd-code.org/pmd-regression-tester/', - # '--debug', + #'--debug', ] begin @summary = PmdTester::Runner.new(argv).run @@ -39,7 +39,7 @@ def upload_report `tar -cf #{tar_filename} diff/` report_url = `curl -u #{ENV['PMD_CI_CHUNK_TOKEN']} -T #{tar_filename} https://chunk.io` if $?.success? - @logger.info "Successfully uploaded #{tar_filename} to chunk.io" + @logger.info "Successfully uploaded #{tar_filename} to #{report_url}" # set value of sticky to true and the message is kept after new commits are submitted to the PR message("This changeset " \ diff --git a/Gemfile b/Gemfile index b8861eb9c3..67b22b628d 100644 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org/' # bleeding edge from git #gem 'pmdtester', :git => 'https://github.com/pmd/pmd-regression-tester.git' -gem 'pmdtester', '~> 1.1' +gem 'pmdtester', '~> 1' gem 'danger', '~> 5.6', '>= 5.6' # This group is only needed for rendering release notes diff --git a/Gemfile.lock b/Gemfile.lock index 9bdbcf639f..e742be01b1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -31,36 +31,38 @@ GEM multipart-post (>= 1.2, < 3) faraday-http-cache (1.3.1) faraday (~> 0.8) - fugit (1.4.1) + fugit (1.4.2) et-orbi (~> 1.1, >= 1.1.8) raabro (~> 1.4) - git (1.7.0) + git (1.8.1) rchardet (~> 1.8) kramdown (1.17.0) - liquid (4.0.3) + liquid (5.0.0) logger-colors (1.0.0) - mini_portile2 (2.4.0) + mini_portile2 (2.5.0) multipart-post (2.1.1) nap (1.1.0) no_proxy_fix (0.1.2) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - octokit (4.19.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + octokit (4.20.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) open4 (1.3.4) - pmdtester (1.1.0) + pmdtester (1.1.1) differ (~> 0.1) liquid (>= 4.0) logger-colors (~> 1.0) - nokogiri (~> 1.8) + nokogiri (>= 1.11.0.rc4) rufus-scheduler (~> 3.5) slop (~> 4.6) public_suffix (4.0.6) raabro (1.4.0) + racc (1.5.2) rchardet (1.8.0) - rouge (3.25.0) - rufus-scheduler (3.6.0) + rouge (3.26.0) + rufus-scheduler (3.7.0) fugit (~> 1.1, >= 1.1.6) safe_yaml (1.0.5) sawyer (0.8.2) @@ -69,7 +71,7 @@ GEM slop (4.8.2) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - tzinfo (2.0.3) + tzinfo (2.0.4) concurrent-ruby (~> 1.0) unicode-display_width (1.7.0) @@ -79,7 +81,7 @@ PLATFORMS DEPENDENCIES danger (~> 5.6, >= 5.6) liquid (>= 4.0.0) - pmdtester (~> 1.1) + pmdtester (~> 1) rouge (>= 1.7, < 4) safe_yaml (>= 1.0) diff --git a/README.md b/README.md index ca8a2941b8..f639615e8a 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,12 @@ Objective-C, Perl, PHP, PLSQL, Python, Ruby, Salesforce.com Apex, Scala, Swift, ## Support -* How do I? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd). -* I got this error, why? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd). +* How do I? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd) + or on [discussions](https://github.com/pmd/pmd/discussions). +* I got this error, why? -- Ask a question on [StackOverflow](https://stackoverflow.com/questions/tagged/pmd) + or on [discussions](https://github.com/pmd/pmd/discussions). * I got this error and I'm sure it's a bug -- file an [issue](https://github.com/pmd/pmd/issues). -* I have an idea/request/question -- file an [issue](https://github.com/pmd/pmd/issues). +* I have an idea/request/question -- create a new [discussion](https://github.com/pmd/pmd/discussions). * I have a quick question -- ask on our [Gitter chat](https://gitter.im/pmd/pmd). * Where's your documentation? -- diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 739f66ebe2..e474166447 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - activesupport (6.0.3.2) + activesupport (6.0.3.4) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -17,37 +17,40 @@ GEM commonmarker (0.17.13) ruby-enum (~> 0.5) concurrent-ruby (1.1.7) - dnsruby (1.61.4) + dnsruby (1.61.5) simpleidn (~> 0.1) - em-websocket (0.5.1) + em-websocket (0.5.2) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) ethon (0.12.0) ffi (>= 1.3.0) eventmachine (1.2.7) execjs (2.7.0) - faraday (1.0.1) + faraday (1.3.0) + faraday-net_http (~> 1.0) multipart-post (>= 1.2, < 3) - ffi (1.13.1) + ruby2_keywords + faraday-net_http (1.0.0) + ffi (1.14.2) forwardable-extended (2.6.0) gemoji (3.0.1) - github-pages (207) + github-pages (209) github-pages-health-check (= 1.16.1) jekyll (= 3.9.0) jekyll-avatar (= 0.7.0) jekyll-coffeescript (= 1.1.1) jekyll-commonmark-ghpages (= 0.1.6) jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.13.0) + jekyll-feed (= 0.15.1) jekyll-gist (= 1.5.0) jekyll-github-metadata (= 2.13.0) - jekyll-mentions (= 1.5.1) + jekyll-mentions (= 1.6.0) jekyll-optional-front-matter (= 0.3.2) jekyll-paginate (= 1.1.0) jekyll-readme-index (= 0.3.0) - jekyll-redirect-from (= 0.15.0) + jekyll-redirect-from (= 0.16.0) jekyll-relative-links (= 0.6.1) - jekyll-remote-theme (= 0.4.1) + jekyll-remote-theme (= 0.4.2) jekyll-sass-converter (= 1.5.2) jekyll-seo-tag (= 2.6.1) jekyll-sitemap (= 1.4.0) @@ -55,7 +58,7 @@ GEM jekyll-theme-architect (= 0.1.1) jekyll-theme-cayman (= 0.1.1) jekyll-theme-dinky (= 0.1.1) - jekyll-theme-hacker (= 0.1.1) + jekyll-theme-hacker (= 0.1.2) jekyll-theme-leap-day (= 0.1.1) jekyll-theme-merlot (= 0.1.1) jekyll-theme-midnight (= 0.1.1) @@ -66,14 +69,14 @@ GEM jekyll-theme-tactile (= 0.1.1) jekyll-theme-time-machine (= 0.1.1) jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.11.1) + jemoji (= 0.12.0) kramdown (= 2.3.0) kramdown-parser-gfm (= 1.1.0) liquid (= 4.0.3) mercenary (~> 0.3) minima (= 2.5.1) nokogiri (>= 1.10.4, < 2.0) - rouge (= 3.19.0) + rouge (= 3.23.0) terminal-table (~> 1.4) github-pages-health-check (1.16.1) addressable (~> 2.3) @@ -114,14 +117,14 @@ GEM rouge (>= 2.0, < 4.0) jekyll-default-layout (0.1.4) jekyll (~> 3.0) - jekyll-feed (0.13.0) + jekyll-feed (0.15.1) jekyll (>= 3.7, < 5.0) jekyll-gist (1.5.0) octokit (~> 4.2) jekyll-github-metadata (2.13.0) jekyll (>= 3.4, < 5.0) octokit (~> 4.0, != 4.4.0) - jekyll-mentions (1.5.1) + jekyll-mentions (1.6.0) html-pipeline (~> 2.3) jekyll (>= 3.7, < 5.0) jekyll-optional-front-matter (0.3.2) @@ -129,14 +132,15 @@ GEM jekyll-paginate (1.1.0) jekyll-readme-index (0.3.0) jekyll (>= 3.0, < 5.0) - jekyll-redirect-from (0.15.0) + jekyll-redirect-from (0.16.0) jekyll (>= 3.3, < 5.0) jekyll-relative-links (0.6.1) jekyll (>= 3.3, < 5.0) - jekyll-remote-theme (0.4.1) + jekyll-remote-theme (0.4.2) addressable (~> 2.0) jekyll (>= 3.5, < 5.0) - rubyzip (>= 1.3.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) jekyll-seo-tag (2.6.1) @@ -153,8 +157,8 @@ GEM jekyll-theme-dinky (0.1.1) jekyll (~> 3.5) jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.1.1) - jekyll (~> 3.5) + jekyll-theme-hacker (0.1.2) + jekyll (> 3.5, < 5.0) jekyll-seo-tag (~> 2.0) jekyll-theme-leap-day (0.1.1) jekyll (~> 3.5) @@ -188,7 +192,7 @@ GEM jekyll (>= 3.3, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - jemoji (0.11.1) + jemoji (0.12.0) gemoji (~> 3.0) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) @@ -197,32 +201,35 @@ GEM kramdown-parser-gfm (1.1.0) kramdown (~> 2.0) liquid (4.0.3) - listen (3.2.1) + listen (3.4.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) - mini_portile2 (2.4.0) + mini_portile2 (2.5.0) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) - minitest (5.14.1) + minitest (5.14.3) multipart-post (2.1.1) - nokogiri (1.10.10) - mini_portile2 (~> 2.4.0) - octokit (4.18.0) + nokogiri (1.11.1) + mini_portile2 (~> 2.5.0) + racc (~> 1.4) + octokit (4.20.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (3.1.1) + racc (1.5.2) rb-fsevent (0.10.4) rb-inotify (0.10.1) ffi (~> 1.0) rexml (3.2.4) - rouge (3.19.0) + rouge (3.23.0) ruby-enum (0.8.0) i18n + ruby2_keywords (0.0.2) rubyzip (2.3.0) safe_yaml (1.0.5) sass (3.7.4) @@ -240,13 +247,13 @@ GEM thread_safe (0.3.6) typhoeus (1.4.0) ethon (>= 0.9.0) - tzinfo (1.2.7) + tzinfo (1.2.9) thread_safe (~> 0.1) unf (0.1.4) unf_ext unf_ext (0.0.7.7) unicode-display_width (1.7.0) - zeitwerk (2.4.0) + zeitwerk (2.4.2) PLATFORMS ruby diff --git a/docs/_config.yml b/docs/_config.yml index 220abe44ad..61ed05eb78 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,9 +1,9 @@ repository: pmd/pmd pmd: - version: 6.31.0-SNAPSHOT - previous_version: 6.30.0 - date: ??-January-2021 + version: 6.32.0-SNAPSHOT + previous_version: 6.31.0 + date: ??-February-2021 release_type: minor # release types: major, minor, bugfix diff --git a/docs/pages/next_major_development.md b/docs/pages/next_major_development.md index d8b0d6a550..8ca17e194d 100644 --- a/docs/pages/next_major_development.md +++ b/docs/pages/next_major_development.md @@ -125,13 +125,29 @@ the breaking API changes will be performed in 7.0.0. an API is tagged as `@Deprecated` or not in the latest minor release. During the development of 7.0.0, we may decide to remove some APIs that were not tagged as deprecated, though we'll try to avoid it." %} +#### 6.31.0 + +##### Deprecated API + +* {% jdoc xml::lang.xml.rule.AbstractDomXmlRule %} +* {% jdoc xml::lang.wsdl.rule.AbstractWsdlRule %} +* A few methods of {% jdoc xml::lang.xml.rule.AbstractXmlRule %} + +##### Experimental APIs + +* The method {% jdoc !!core::lang.ast.GenericToken#getKind() %} has been added as experimental. This + unifies the token interface for both JavaCC and Antlr. The already existing method + {% jdoc !!core::cpd.token.AntlrToken#getKind() %} is therefore experimental as well. The + returned constant depends on the actual language and might change whenever the grammar + of the language is changed. + #### 6.30.0 ##### Deprecated API ###### Around RuleSet parsing -* {% jdoc core::RuleSetFactory %} and {% jdoc core::RuleSetFactoryUtils %} have been deprecated in favor of {% jdoc core::RuleSetLoader %}. This is easier to configure, and more maintainable than the multiple overloads of `RuleSetFactoryUtils`. +* {% jdoc core::RuleSetFactory %} and {% jdoc core::RulesetsFactoryUtils %} have been deprecated in favor of {% jdoc core::RuleSetLoader %}. This is easier to configure, and more maintainable than the multiple overloads of `RulesetsFactoryUtils`. * Some static creation methods have been added to {% jdoc core::RuleSet %} for simple cases, eg {% jdoc core::RuleSet#forSingleRule(core::Rule) %}. These replace some counterparts in {% jdoc core::RuleSetFactory %} * Since {% jdoc core::RuleSets %} is also deprecated, many APIs that require a RuleSets instance now are deprecated, and have a counterpart that expects a `List`. * {% jdoc core::RuleSetReferenceId %}, {% jdoc core::RuleSetReference %}, {% jdoc core::RuleSetFactoryCompatibility %} are deprecated. They are most likely not relevant outside of the implementation of pmd-core. diff --git a/docs/pages/pmd/about/help.md b/docs/pages/pmd/about/help.md index c20f9fd066..8e45c96b8b 100644 --- a/docs/pages/pmd/about/help.md +++ b/docs/pages/pmd/about/help.md @@ -1,8 +1,8 @@ --- title: Getting Help permalink: pmd_about_help.html -author: Andreas Dangel -last_updated: September 2017 +author: Andreas Dangel +last_updated: January 2021 --- There are numerous ways of getting help: @@ -13,7 +13,7 @@ There are numerous ways of getting help: * If you found a bug, please create a new [github issue](https://github.com/pmd/pmd/issues). -* You can also ask questions in our [sourceforge forum](https://sourceforge.net/p/pmd/discussion/). +* You can also ask questions on [github discussions](https://github.com/pmd/pmd/discussions). * Or you can join the [Mailing List](https://lists.sourceforge.net/lists/listinfo/pmd-devel) or browse through the archives ([archive1](http://java-pmd.30631.n5.nabble.com/), [archive2](http://web.archive.org/web/20160715035623/http://blog.gmane.org:80/gmane.comp.java.audit.pmd.devel)). diff --git a/docs/pages/pmd/projectdocs/trivia/news.md b/docs/pages/pmd/projectdocs/trivia/news.md index 52f114c4af..8fb06bbd4c 100644 --- a/docs/pages/pmd/projectdocs/trivia/news.md +++ b/docs/pages/pmd/projectdocs/trivia/news.md @@ -26,6 +26,8 @@ author: Tom Copeland ### PMD in general and other Language Modules +* February 2021 - Artem Krosheninnikov's talk about Quality Assurance Automation: [Artem Krosheninnikov, Wrike - How static analysis can help in QAA processes](https://www.youtube.com/watch?v=L42zH5ne074) + * May 2019 - [Code quality assurance with PMD – An extensible static code analyser for Java and other languages](https://www.datarespons.com/code-quality-assurance-with-pmd/) * February 2012 - Romain Pelisse's lightning talk at FOSDEM 2012 about "PMD5: What can it do for you?". diff --git a/docs/pages/pmd/userdocs/configuring_rules.md b/docs/pages/pmd/userdocs/configuring_rules.md index aad977668c..bec948aaa5 100644 --- a/docs/pages/pmd/userdocs/configuring_rules.md +++ b/docs/pages/pmd/userdocs/configuring_rules.md @@ -60,8 +60,8 @@ All property assignments must be enclosed in a `properties` element, which is it Some properties take multiple values (a list), in which case you can provide them all by delimiting them with a delimiter character. It is usually a pipe ('\|'), or a comma (',') for numeric properties, e.g. ```xml - + ``` These properties are referred to as **multivalued properties** in this documentation. diff --git a/docs/pages/pmd/userdocs/cpd/cpd.md b/docs/pages/pmd/userdocs/cpd/cpd.md index 8d94294926..c5e142341d 100644 --- a/docs/pages/pmd/userdocs/cpd/cpd.md +++ b/docs/pages/pmd/userdocs/cpd/cpd.md @@ -119,7 +119,7 @@ Novice as much as advanced readers may want to [read on on Refactoring Guru](htt {% include custom/cli_option_row.html options="--ignore-literal-sequences" description="Ignore sequences of literals (common e.g. in list initializers)" default="false" - languages="C#" + languages="C#, C++" %} {% include custom/cli_option_row.html options="--ignore-usings" description="Ignore `using` directives in C# when comparing text" @@ -253,14 +253,14 @@ For details, see [CPD Report Formats](pmd_userdocs_cpd_report_formats.html). Andy Glover wrote an Ant task for CPD; here's how to use it: ```xml - - - - - - - - + + + + + + + + ``` @@ -352,7 +352,7 @@ Also, you can get an HTML report from CPD by using the XSLT script in pmd/etc/xs the CPD task as usual and right after it invoke the Ant XSLT script like this: ```xml - + ``` ## GUI @@ -378,23 +378,23 @@ Arbitrary blocks of code can be ignored through comments on **Java**, **C/C++**, **Kotlin**, **Lua**, **Matlab**, **Objective-C**, **PL/SQL**, **Python**, **Scala**, **Swift** and **C#** by including the keywords `CPD-OFF` and `CPD-ON`. ```java - public Object someParameterizedFactoryMethod(int x) throws Exception { - // some unignored code +public Object someParameterizedFactoryMethod(int x) throws Exception { + // some unignored code - // tell cpd to start ignoring code - CPD-OFF + // tell cpd to start ignoring code - CPD-OFF - // mission critical code, manually loop unroll - goDoSomethingAwesome(x + x / 2); - goDoSomethingAwesome(x + x / 2); - goDoSomethingAwesome(x + x / 2); - goDoSomethingAwesome(x + x / 2); - goDoSomethingAwesome(x + x / 2); - goDoSomethingAwesome(x + x / 2); + // mission critical code, manually loop unroll + goDoSomethingAwesome(x + x / 2); + goDoSomethingAwesome(x + x / 2); + goDoSomethingAwesome(x + x / 2); + goDoSomethingAwesome(x + x / 2); + goDoSomethingAwesome(x + x / 2); + goDoSomethingAwesome(x + x / 2); - // resume CPD analysis - CPD-ON + // resume CPD analysis - CPD-ON - // further code will *not* be ignored - } + // further code will *not* be ignored +} ``` Additionally, **Java** allows to toggle suppression by adding the annotations @@ -405,15 +405,15 @@ This approach however, is limited to the locations were `@SuppressWarnings` is a It's legacy and the new comment's based approach should be favored. ```java - //enable suppression - @SuppressWarnings("CPD-START") - public Object someParameterizedFactoryMethod(int x) throws Exception { - // any code here will be ignored for the duplication detection - } - //disable suppression - @SuppressWarnings("CPD-END) - public void nextMethod() { - } +//enable suppression +@SuppressWarnings("CPD-START") +public Object someParameterizedFactoryMethod(int x) throws Exception { + // any code here will be ignored for the duplication detection +} +//disable suppression +@SuppressWarnings("CPD-END) +public void nextMethod() { +} ``` Other languages currently have no support to suppress CPD reports. In the future, diff --git a/docs/pages/pmd/userdocs/pmd_report_formats.md b/docs/pages/pmd/userdocs/pmd_report_formats.md index 5ca846ac69..8f34c1192e 100644 --- a/docs/pages/pmd/userdocs/pmd_report_formats.md +++ b/docs/pages/pmd/userdocs/pmd_report_formats.md @@ -19,6 +19,18 @@ those can be specified with the `-property` / `-P` option on the command-line. {% include note.html content="Suppressed violations are only reported, if the CLI parameter `-showsuppressed` is set." %} +## sarif + +"SARIF, the Static Analysis Results Interchange Format, is a standard, JSON-based format for the output of static +analysis tools. It has been approved as an OASIS standard" - . + +SARIF schema can be found here: . + +PMD holds an initial implementation version of SARIF rendering. This means SARIF allows for more complexity and the +current implementation can be extended. + +[Example](report-examples/pmd-report.sarif.json) + ## codeclimate Renderer for Code Climate JSON format. diff --git a/docs/pages/pmd/userdocs/tools/maven.md b/docs/pages/pmd/userdocs/tools/maven.md index d9a13cc468..d7998bdfd4 100644 --- a/docs/pages/pmd/userdocs/tools/maven.md +++ b/docs/pages/pmd/userdocs/tools/maven.md @@ -26,17 +26,17 @@ report additionally in `` elements. Here's an e section: ```xml - - - - - org.apache.maven.plugins - maven-pmd-plugin - {{ page.mpmd_version }} - - - - + + + + + org.apache.maven.plugins + maven-pmd-plugin + {{ page.mpmd_version }} + + + + ``` When defining the version in the pluginManagment section, then it doesn't need to be specified in the normal plugins @@ -123,22 +123,22 @@ To specify a ruleset, simply edit the previous configuration: ``` xml - - - - org.apache.maven.plugins - maven-pmd-plugin - {{ page.mpmd_version }} - - - /rulesets/java/quickstart.xml - d:\rulesets\my-ruleset.xml - http://localhost/design.xml - - - - - + + + + org.apache.maven.plugins + maven-pmd-plugin + {{ page.mpmd_version }} + + + /rulesets/java/quickstart.xml + d:\rulesets\my-ruleset.xml + http://localhost/design.xml + + + + + ``` The value of the 'ruleset' element can either be a relative address, an absolute address or even an url. @@ -156,17 +156,17 @@ When using the Maven PMD plugin 3.8 or later along with PMD 5.6.0 or later, you speed up PMD's execution while retaining the quality of the analysis. You can additionally customize where the cache is stored:: ```xml - - org.apache.maven.plugins - maven-pmd-plugin - {{ page.mpmd_version }} - - - true - - ${project.build.directory}/pmd/pmd.cache - - + + org.apache.maven.plugins + maven-pmd-plugin + {{ page.mpmd_version }} + + + true + + ${project.build.directory}/pmd/pmd.cache + + ``` #### Other configurations @@ -175,17 +175,17 @@ The Maven PMD plugin allows you to configure CPD, targetJDK, and the use of XRef the report to html source files, and the file encoding: ```xml - - org.apache.maven.plugins - maven-pmd-plugin - {{ page.mpmd_version }} - - true - ISO-8859-1 - 30 - 1.4 - - + + org.apache.maven.plugins + maven-pmd-plugin + {{ page.mpmd_version }} + + true + ISO-8859-1 + 30 + 1.4 + + ``` #### Upgrading the PMD version at runtime diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md index b8f8783555..0db79e7282 100644 --- a/docs/pages/release_notes.md +++ b/docs/pages/release_notes.md @@ -14,11 +14,74 @@ This is a {{ site.pmd.release_type }} release. ### New and noteworthy +#### Java 16 Support + +This release of PMD brings support for Java 16. PMD supports [JEP 394: Pattern Matching for instanceof](https://openjdk.java.net/jeps/394) and [JEP 395: Records](https://openjdk.java.net/jeps/395). Both have been promoted +to be a standard language feature of Java 16. + +PMD also supports [JEP 397: Sealed Classes (Second Preview)](https://openjdk.java.net/jeps/397) as a preview +language feature. In order to analyze a project with PMD that uses these language features, you'll need to enable +it via the environment variable `PMD_JAVA_OPTS` and select the new language version `16-preview`: + + export PMD_JAVA_OPTS=--enable-preview + ./run.sh pmd -language java -version 16-preview ... + +Note: Support for Java 14 preview language features have been removed. The version "14-preview" is no longer available. + +#### Modified Rules + +* The Apex rule {% rule "apex/documentation/ApexDoc" %} has two new properties: `reportPrivate` and + `reportProtected`. Previously the rule only considered public and global classes, methods, and + properties. With these properties, you can verify the existence of ApexDoc comments for private + and protected methods as well. By default, these properties are disabled to preserve backwards + compatible behavior. + ### Fixed Issues +* apex-documentation + * [#3075](https://github.com/pmd/pmd/issues/3075): \[apex] ApexDoc should support private access modifier +* java + * [#3101](https://github.com/pmd/pmd/issues/3101): \[java] NullPointerException when running PMD under JRE 11 +* java-bestpractices + * [#3132](https://github.com/pmd/pmd/issues/3132): \[java] UnusedImports with static imports on subclasses +* java-errorprone + * [#2716](https://github.com/pmd/pmd/issues/2716): \[java] CompareObjectsWithEqualsRule: False positive with Enums + * [#3089](https://github.com/pmd/pmd/issues/3089): \[java] CloseResource rule throws exception on spaces in property types + * [#3133](https://github.com/pmd/pmd/issues/3133): \[java] InvalidLogMessageFormat FP with StringFormattedMessage and ParameterizedMessage +* plsql + * [#3106](https://github.com/pmd/pmd/issues/3106): \[plsql] ParseException while parsing EXECUTE IMMEDIATE 'drop database link ' \|\| linkname; + ### API Changes +#### Experimental APIs + +* The experimental class `ASTTypeTestPattern` has been renamed to {% jdoc java::lang.java.ast.ASTTypePattern %} + in order to align the naming to the JLS. +* The experimental class `ASTRecordConstructorDeclaration` has been renamed to {% jdoc java::lang.java.ast.ASTCompactConstructorDeclaration %} + in order to align the naming to the JLS. +* The AST types and APIs around Pattern Matching and Records are not experimental anymore: + * {% jdoc !!java::lang.java.ast.ASTVariableDeclaratorId#isPatternBinding() %} + * {% jdoc java::lang.java.ast.ASTPattern %} + * {% jdoc java::lang.java.ast.ASTTypePattern %} + * {% jdoc java::lang.java.ast.ASTRecordDeclaration %} + * {% jdoc java::lang.java.ast.ASTRecordComponentList %} + * {% jdoc java::lang.java.ast.ASTRecordComponent %} + * {% jdoc java::lang.java.ast.ASTRecordBody %} + * {% jdoc java::lang.java.ast.ASTCompactConstructorDeclaration %} + +#### Internal API + +Those APIs are not intended to be used by clients, and will be hidden or removed with PMD 7.0.0. +You can identify them with the `@InternalApi` annotation. You'll also get a deprecation warning. + +* The protected or public member of the Java rule {% jdoc java::lang.java.rule.bestpractices.AvoidUsingHardCodedIPRule %} + are deprecated and considered to be internal API. They will be removed with PMD 7. + ### External Contributions +* [#3098](https://github.com/pmd/pmd/pull/3098): \[apex] ApexDoc optionally report private and protected - [Jonathan Wiesel](https://github.com/jonathanwiesel) +* [#3107](https://github.com/pmd/pmd/pull/3107): \[plsql] Fix ParseException for EXECUTE IMMEDIATE str1\|\|str2; - [hvbtup](https://github.com/hvbtup) +* [#3125](https://github.com/pmd/pmd/pull/3125): \[doc] Fix sample code indentation in documentation - [Artur Dryomov](https://github.com/arturdryomov) + {% endtocmaker %} diff --git a/docs/pages/release_notes_old.md b/docs/pages/release_notes_old.md index cc2bb4a7f7..d06ea6ccf0 100644 --- a/docs/pages/release_notes_old.md +++ b/docs/pages/release_notes_old.md @@ -5,6 +5,123 @@ permalink: pmd_release_notes_old.html Previous versions of PMD can be downloaded here: https://github.com/pmd/pmd/releases +## 30-January-2021 - 6.31.0 + +The PMD team is pleased to announce PMD 6.31.0. + +This is a minor release. + +### Table Of Contents + +* [New and noteworthy](#new-and-noteworthy) + * [SARIF Format](#sarif-format) + * [CPD](#cpd) + * [New Rules](#new-rules) + * [Deprecated rules](#deprecated-rules) +* [Fixed Issues](#fixed-issues) +* [API Changes](#api-changes) + * [Deprecated API](#deprecated-api) + * [Experimental APIs](#experimental-apis) +* [External Contributions](#external-contributions) +* [Stats](#stats) + +### New and noteworthy + +#### SARIF Format + +PMD now supports the [Static Analysis Results Interchange Format (SARIF)](https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=sarif) +as an additional report format. Just use the [command line parameter](pmd_userdocs_cli_reference.html#format) `-format sarif` to select it. +SARIF is an OASIS standard format for static analysis tools. +PMD creates SARIF JSON files in [SARIF version 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html). +An example report can be found in the documentation in [Report formats for PMD](pmd_userdocs_report_formats.html#sarif). + +#### CPD + +* The C++ module now supports the new option [`--ignore-literal-sequences`](https://pmd.github.io/latest/pmd_userdocs_cpd.html#-ignore-literal-sequences), + which can be used to avoid detection of some uninteresting clones. This options has been + introduced with PMD 6.30.0 for C# and is now available for C++ as well. See [#2963](https://github.com/pmd/pmd/pull/2963). + +#### New Rules + +* The new Apex rule [`OverrideBothEqualsAndHashcode`](https://pmd.github.io/pmd-6.31.0/pmd_rules_apex_errorprone.html#overridebothequalsandhashcode) brings the well known Java rule + to Apex. In Apex the same principle applies: `equals` and `hashCode` should always be overridden + together to ensure collection classes such as Maps and Sets work as expected. + +* The new Visualforce rule [`VfHtmlStyleTagXss`](https://pmd.github.io/pmd-6.31.0/pmd_rules_vf_security.html#vfhtmlstyletagxss) checks for potential XSS problems + when using ` content. + */ + private void verifyEncoding( + ASTElExpression node, + ASTContent contentNode, + ASTElement elementNode, + Object data) { + final String previousText = getPreviousText(contentNode, node); + final boolean isWithinSafeResource = ElEscapeDetector.startsWithSafeResource(node); + + // if El is inside a tag + // and is not surrounded by a safe resource, check for violations + if (isStyleTag(elementNode) && !isWithinSafeResource) { + + // check if we are within a URL expression + if (isWithinUrlMethod(previousText)) { + verifyEncodingWithinUrl(node, data); + } else { + verifyEncodingWithoutUrl(node, data); + } + } + } + + private boolean isStyleTag(ASTElement elementNode) { + // are we dealing with HTML tag? + return STYLE_TAG.equalsIgnoreCase(elementNode.getLocalName()); + } + + private void verifyEncodingWithinUrl(ASTElExpression elExpressionNode, Object data) { + + // only allow URLENCODING or JSINHTMLENCODING + if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers( + elExpressionNode, + URLENCODE_JSINHTMLENCODE)) { + addViolationWithMessage( + data, + elExpressionNode, + "Dynamic EL content within URL in style tag should be URLENCODED or JSINHTMLENCODED as appropriate"); + } + + } + + private void verifyEncodingWithoutUrl(ASTElExpression elExpressionNode, Object data) { + if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers( + elExpressionNode, + ANY_ENCODE)) { + addViolationWithMessage( + data, + elExpressionNode, + "Dynamic EL content in style tag should be appropriately encoded"); + } + } + + private boolean isApexPrefixed(ASTElement node) { + return node.isHasNamespacePrefix() + && APEX_PREFIX.equalsIgnoreCase(node.getNamespacePrefix()); + } + + /** + * Get text content within style tag that leads up to the ElExpression. + * For example, in this snippet: + * + *
+     * <style>
+     *  div {
+     *   background: url('{!HTMLENCODE(XSSHere)}');
+     * }
+     * </style>
+     * 
+ * + * {@code getPreviousText(...)} would return "\n div {\n background: url(". + * + */ + private String getPreviousText(ASTContent content, ASTElExpression elExpressionNode) { + final int indexInParent = elExpressionNode.getIndexInParent(); + final VfNode previous = indexInParent > 0 ? content.getChild(indexInParent - 1) : null; + return previous instanceof ASTText ? previous.getImage() : ""; + } + + // visible for unit testing + static boolean isWithinUrlMethod(String previousText) { + // match for a pattern that + // 1. contains "url" (case insensitive), + // 2. followed by any number of whitespaces, + // 3. a starting bracket "(" + // 4. and anything else but an ending bracket ")" + // For example: + // Matches: "div { background: url('", "div { background: Url ( blah" + // Does not match: "div { background: url('myUrl')", "div { background: myStyle('" + + return URL_METHOD_PATTERN.matcher(previousText).find(); + } + +} diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java index 77527770e5..fa4a4b4491 100644 --- a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/VfUnescapeElRule.java @@ -12,22 +12,17 @@ import java.util.Set; import java.util.regex.Pattern; import net.sourceforge.pmd.lang.ast.Node; -import net.sourceforge.pmd.lang.vf.DataType; -import net.sourceforge.pmd.lang.vf.ast.ASTArguments; import net.sourceforge.pmd.lang.vf.ast.ASTAttribute; import net.sourceforge.pmd.lang.vf.ast.ASTContent; -import net.sourceforge.pmd.lang.vf.ast.ASTDotExpression; import net.sourceforge.pmd.lang.vf.ast.ASTElExpression; import net.sourceforge.pmd.lang.vf.ast.ASTElement; import net.sourceforge.pmd.lang.vf.ast.ASTExpression; import net.sourceforge.pmd.lang.vf.ast.ASTHtmlScript; -import net.sourceforge.pmd.lang.vf.ast.ASTIdentifier; import net.sourceforge.pmd.lang.vf.ast.ASTLiteral; -import net.sourceforge.pmd.lang.vf.ast.ASTNegationExpression; import net.sourceforge.pmd.lang.vf.ast.ASTText; -import net.sourceforge.pmd.lang.vf.ast.AbstractVFNode; -import net.sourceforge.pmd.lang.vf.ast.VfTypedNode; import net.sourceforge.pmd.lang.vf.rule.AbstractVfRule; +import net.sourceforge.pmd.lang.vf.rule.security.internal.ElEscapeDetector; + /** * @author sergey.gorbaty February 2017 @@ -51,83 +46,44 @@ public class VfUnescapeElRule extends AbstractVfRule { private static final String FALSE = "false"; private static final Pattern ON_EVENT = Pattern.compile("^on(\\w)+$"); private static final Pattern PLACEHOLDERS = Pattern.compile("\\{(\\w|,|\\.|'|:|\\s)*\\}"); + private static final EnumSet JSENCODE_JSINHTMLENCODE = EnumSet.of(ElEscapeDetector.Escaping.JSENCODE, ElEscapeDetector.Escaping.JSINHTMLENCODE); + private static final EnumSet ANY_ENCODE = EnumSet.of(ElEscapeDetector.Escaping.ANY); @Override public Object visit(ASTHtmlScript node, Object data) { checkIfCorrectlyEscaped(node, data); + return super.visit(node, data); } private void checkIfCorrectlyEscaped(ASTHtmlScript node, Object data) { - ASTText prevText = null; - // churn thru every child just once instead of twice for (int i = 0; i < node.getNumChildren(); i++) { Node n = node.getChild(i); - if (n instanceof ASTText) { - prevText = (ASTText) n; - continue; - } - if (n instanceof ASTElExpression) { - processElInScriptContext((ASTElExpression) n, prevText, data); + processElInScriptContext((ASTElExpression) n, data); } } } - private void processElInScriptContext(ASTElExpression elExpression, ASTText prevText, Object data) { - boolean quoted = false; - boolean jsonParse = false; - - if (prevText != null) { - jsonParse = isJsonParse(prevText); - if (isUnbalanced(prevText.getImage(), '\'') || isUnbalanced(prevText.getImage(), '\"')) { - quoted = true; - } - } - if (quoted) { - // check escaping too - if (!(jsonParse || startsWithSafeResource(elExpression) || containsSafeFields(elExpression))) { - if (doesElContainAnyUnescapedIdentifiers(elExpression, - EnumSet.of(Escaping.JSENCODE, Escaping.JSINHTMLENCODE))) { - addViolation(data, elExpression); - } - } - } else { - if (!(startsWithSafeResource(elExpression) || containsSafeFields(elExpression))) { - final boolean hasUnscaped = doesElContainAnyUnescapedIdentifiers(elExpression, - EnumSet.of(Escaping.JSENCODE, Escaping.JSINHTMLENCODE)); - if (!(jsonParse && !hasUnscaped)) { - addViolation(data, elExpression); - } - } + private void processElInScriptContext(ASTElExpression elExpression, Object data) { + if (!properlyEscaped(elExpression)) { + addViolation(data, elExpression); } } - private boolean isJsonParse(ASTText prevText) { - final String text = prevText.getImage().endsWith("'") - ? prevText.getImage().substring(0, prevText.getImage().length() - 1) : prevText.getImage(); + private boolean properlyEscaped(ASTElExpression el) { + // Find the first Expression-type child of this top-level node. + ASTExpression expression = el.getFirstChildOfType(ASTExpression.class); - return text.endsWith("JSON.parse(") || text.endsWith("jQuery.parseJSON(") || text.endsWith("$.parseJSON("); - } - - private boolean isUnbalanced(String image, char pattern) { - char[] array = image.toCharArray(); - - boolean foundPattern = false; - - for (int i = array.length - 1; i > 0; i--) { - if (array[i] == pattern) { - foundPattern = true; - } - - if (array[i] == ';') { - return foundPattern; - } + // If no such node was found, then there's nothing to escape, so we're fine. + if (expression == null) { + return true; } - return foundPattern; + // Otherwise, we should pass the expression node into our recursive checker. + return ElEscapeDetector.expressionRecursivelyValid(expression, JSENCODE_JSINHTMLENCODE); } @Override @@ -181,11 +137,11 @@ public class VfUnescapeElRule extends AbstractVfRule { break; } - if (startsWithSafeResource(el)) { + if (ElEscapeDetector.startsWithSafeResource(el)) { break; } - if (doesElContainAnyUnescapedIdentifiers(el, Escaping.URLENCODE)) { + if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el, ElEscapeDetector.Escaping.URLENCODE)) { isEL = true; toReport.add(el); } @@ -216,12 +172,11 @@ public class VfUnescapeElRule extends AbstractVfRule { if (ON_EVENT.matcher(name).matches()) { final List elsInVal = attr.findDescendantsOfType(ASTElExpression.class); for (ASTElExpression el : elsInVal) { - if (startsWithSafeResource(el)) { + if (ElEscapeDetector.startsWithSafeResource(el)) { continue; } - if (doesElContainAnyUnescapedIdentifiers(el, - EnumSet.of(Escaping.ANY))) { + if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el, ANY_ENCODE)) { isEL = true; toReport.add(el); } @@ -238,78 +193,6 @@ public class VfUnescapeElRule extends AbstractVfRule { } - private boolean startsWithSafeResource(final ASTElExpression el) { - final ASTExpression expression = el.getFirstChildOfType(ASTExpression.class); - if (expression != null) { - final ASTNegationExpression negation = expression.getFirstChildOfType(ASTNegationExpression.class); - if (negation != null) { - return true; - } - - final ASTIdentifier id = expression.getFirstChildOfType(ASTIdentifier.class); - if (id != null) { - String lowerCaseId = id.getImage().toLowerCase(Locale.ROOT); - List args = expression.findChildrenOfType(ASTArguments.class); - if (!args.isEmpty()) { - switch (lowerCaseId) { - case "urlfor": - case "casesafeid": - case "begins": - case "contains": - case "len": - case "getrecordids": - case "linkto": - case "sqrt": - case "round": - case "mod": - case "log": - case "ln": - case "exp": - case "abs": - case "floor": - case "ceiling": - case "nullvalue": - case "isnumber": - case "isnull": - case "isnew": - case "isblank": - case "isclone": - case "year": - case "month": - case "day": - case "datetimevalue": - case "datevalue": - case "date": - case "now": - case "today": - return true; - - default: - } - } else { - // has no arguments - switch (lowerCaseId) { - case "$action": - case "$page": - case "$site": - case "$resource": - case "$label": - case "$objecttype": - case "$component": - case "$remoteaction": - case "$messagechannel": - return true; - - default: - } - } - } - - } - - return false; - } - private boolean startsWithSlashLiteral(final ASTElExpression elExpression) { final ASTExpression expression = elExpression.getFirstChildOfType(ASTExpression.class); if (expression != null) { @@ -352,11 +235,12 @@ public class VfUnescapeElRule extends AbstractVfRule { final List elsInVal = attr.findDescendantsOfType(ASTElExpression.class); for (ASTElExpression el : elsInVal) { - if (startsWithSafeResource(el)) { + if (ElEscapeDetector.startsWithSafeResource(el)) { continue; } - if (doesElContainAnyUnescapedIdentifiers(el, Escaping.HTMLENCODE)) { + if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el, + ElEscapeDetector.Escaping.HTMLENCODE)) { isEL = true; toReport.add(el); } @@ -390,117 +274,6 @@ public class VfUnescapeElRule extends AbstractVfRule { } } - private boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, Escaping escape) { - return doesElContainAnyUnescapedIdentifiers(elExpression, EnumSet.of(escape)); - - } - - private boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, - EnumSet escapes) { - if (elExpression == null) { - return false; - } - - final Set nonEscapedIds = new HashSet<>(); - - final List exprs = elExpression.findChildrenOfType(ASTExpression.class); - for (final ASTExpression expr : exprs) { - - if (innerContainsSafeFields(expr)) { - continue; - } - - if (expressionContainsSafeDataNodes(expr)) { - continue; - } - - final List ids = expr.findChildrenOfType(ASTIdentifier.class); - for (final ASTIdentifier id : ids) { - boolean isEscaped = false; - - for (Escaping e : escapes) { - - if (id.getImage().equalsIgnoreCase(e.toString())) { - isEscaped = true; - break; - } - - if (e.equals(Escaping.ANY)) { - for (Escaping esc : Escaping.values()) { - if (id.getImage().equalsIgnoreCase(esc.toString())) { - isEscaped = true; - break; - } - } - } - - } - - if (!isEscaped) { - nonEscapedIds.add(id); - } - } - - } - - return !nonEscapedIds.isEmpty(); - } - - /** - * Return true if the type of all data nodes can be determined and none of them require escaping - */ - private boolean expressionContainsSafeDataNodes(ASTExpression expression) { - try { - for (VfTypedNode node : expression.getDataNodes().keySet()) { - DataType dataType = node.getDataType(); - if (dataType == null || dataType.requiresEscaping) { - return false; - } - } - - return true; - } catch (ASTExpression.DataNodeStateException e) { - return false; - } - } - - private boolean containsSafeFields(final AbstractVFNode expression) { - final ASTExpression ex = expression.getFirstChildOfType(ASTExpression.class); - - return ex != null && innerContainsSafeFields(ex); - - } - - private boolean innerContainsSafeFields(final AbstractVFNode expression) { - for (int i = 0; i < expression.getNumChildren(); i++) { - Node child = expression.getChild(i); - - if (child instanceof ASTIdentifier) { - switch (child.getImage().toLowerCase(Locale.ROOT)) { - case "id": - case "size": - case "caseNumber": - return true; - default: - } - } - - if (child instanceof ASTArguments) { - if (containsSafeFields((ASTArguments) child)) { - return true; - } - } - - if (child instanceof ASTDotExpression) { - if (innerContainsSafeFields((ASTDotExpression) child)) { - return true; - } - } - - } - - return false; - } private boolean doesTagSupportEscaping(final ASTElement node) { if (node.getName() == null) { @@ -530,11 +303,12 @@ public class VfUnescapeElRule extends AbstractVfRule { for (ASTAttribute attrib : innerAttributes) { final List elsInVal = attrib.findDescendantsOfType(ASTElExpression.class); for (final ASTElExpression el : elsInVal) { - if (startsWithSafeResource(el)) { + if (ElEscapeDetector.startsWithSafeResource(el)) { continue; } - if (doesElContainAnyUnescapedIdentifiers(el, Escaping.HTMLENCODE)) { + if (ElEscapeDetector.doesElContainAnyUnescapedIdentifiers(el, + ElEscapeDetector.Escaping.HTMLENCODE)) { toReturn.add(el); } @@ -546,24 +320,4 @@ public class VfUnescapeElRule extends AbstractVfRule { return toReturn; } - - enum Escaping { - HTMLENCODE("HTMLENCODE"), - URLENCODE("URLENCODE"), - JSINHTMLENCODE("JSINHTMLENCODE"), - JSENCODE("JSENCODE"), - ANY("ANY"); - - private final String text; - - Escaping(final String text) { - this.text = text; - } - - @Override - public String toString() { - return text; - } - } - } diff --git a/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/internal/ElEscapeDetector.java b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/internal/ElEscapeDetector.java new file mode 100644 index 0000000000..1cd88abccd --- /dev/null +++ b/pmd-visualforce/src/main/java/net/sourceforge/pmd/lang/vf/rule/security/internal/ElEscapeDetector.java @@ -0,0 +1,404 @@ +/* + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ + +package net.sourceforge.pmd.lang.vf.rule.security.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import net.sourceforge.pmd.lang.vf.DataType; +import net.sourceforge.pmd.lang.vf.ast.ASTArguments; +import net.sourceforge.pmd.lang.vf.ast.ASTDotExpression; +import net.sourceforge.pmd.lang.vf.ast.ASTElExpression; +import net.sourceforge.pmd.lang.vf.ast.ASTExpression; +import net.sourceforge.pmd.lang.vf.ast.ASTIdentifier; +import net.sourceforge.pmd.lang.vf.ast.ASTNegationExpression; +import net.sourceforge.pmd.lang.vf.ast.VfNode; +import net.sourceforge.pmd.lang.vf.ast.VfTypedNode; + +/** + * Helps detect visualforce encoding in EL Expressions + * (porting over code previously living in VfUnescapeElRule for reusability) + */ + +public final class ElEscapeDetector { + + private static final Set SAFE_PROPERTIES = new HashSet<>(Arrays.asList("id", "size", "caseNumber")); + private static final Set SAFE_BUILTIN_FUNCTIONS = new HashSet<>(Arrays.asList( + // These DateTime functions accept or return dates, and therefore don't need escaping. + "addmonths", "date", "datevalue", "datetimevalue", "day", "hour", "millisecond", "minute", "month", "now", + "second", "timenow", "timevalue", "today", "weekday", "year", + // These Logical functions accept or return booleans, and therefore don't need escaping. + "and", "isblank", "isclone", "isnew", "isnull", "isnumber", "not", "or", + // These Math functions return numbers, and therefore don't require escaping. + "abs", "ceiling", "exp", "floor", "ln", "log", "max", "mceiling", "mfloor", "min", "mod", "round", "sqrt", + // These Text functions are safe, either because of what they accept or what they return. + "begins", "br", "casesafeid", "contains", "find", "getsessionid", "ispickval", "len", + // These Advanced functions are safe because of what they accept or what they return. + "currencyrate", "getrecordids", "ischanged", "junctionidlist", "linkto", "regex", "urlfor" + )); + private static final Set FUNCTIONS_WITH_XSSABLE_ARG0 = new HashSet<>(Arrays.asList( + // For these methods, the first argument is a string that must be escaped. + "left", "lower", "lpad", "mid", "right", "rpad", "upper" + )); + private static final Set FUNCTIONS_WITH_XSSABLE_ARG2 = new HashSet<>(Arrays.asList( + // For these methods, the third argument is a string that must be escaped. + "lpad", "rpad" + )); + private static final Set SAFE_GLOBAL_VARS = new HashSet<>(Arrays.asList("$action", "$page", "$site", + "$resource", "$label", "$objecttype", "$component", "$remoteaction", "$messagechannel")); + + private ElEscapeDetector() { + // utility class + } + + private static VfNode getNextSibling(VfNode node) { + VfNode parent = node.getParent(); + if (parent != null && node.getIndexInParent() < (parent.getNumChildren() - 1)) { + return parent.getChild(node.getIndexInParent() + 1); + } + return null; + } + + /** + * Given an ASTExpression node, determines whether that expression and any expressions under it are properly escaped. + * @param expression - Represents a VF expression + * @param escapes - The escape operations that are acceptable in this context + * @return - True if the expression is properly escaped, otherwise false. + */ + public static boolean expressionRecursivelyValid(final ASTExpression expression, final EnumSet escapes) { + // We'll want to iterate over all of this expression's children. + int childCount = expression.getNumChildren(); + String prevId = ""; + List relevantChildren = new ArrayList<>(); + for (int i = 0; i < childCount; i++) { + VfNode child = expression.getChild(i); + + if (child instanceof ASTIdentifier) { + // How we deal with an Identifier depends on what the next node after it is. + VfNode nextNode = getNextSibling(child); + if (nextNode instanceof ASTArguments || nextNode instanceof ASTDotExpression) { + // If the next node is Arguments or Dot expression, that means this Identifier represents the name + // of a function, or some kind of object. In that case, we might be okay. So we'll store the name + // and keep going. + prevId = child.getImage(); + continue; + } + // If there's no next node, or the next node isn't one of the desired types, then this Identifier is a raw + // variable. + if (typedNodeIsSafe((ASTIdentifier) child)) { + // If the raw variable is of an inherently safe type, we can keep going. + continue; + } + // If the raw variable isn't inherently safe, then it's dangerous, and must be escaped. + return false; + } else if (child instanceof ASTArguments) { + // An Arguments node means we're looking at some kind of function call. + // If it's one of our designated escape functions, we're in the clear and we can keep going. + // Also, some built-in functions are inherently safe, which would mean we're good to continue. + if (functionIsEscape(prevId, escapes) || functionInherentlySafe(prevId)) { + continue; + } + + // Otherwise, identify the argument expressions that must be checked, and add them to the list. + relevantChildren.addAll(getXssableArguments(prevId, (ASTArguments) child)); + } else if (child instanceof ASTDotExpression) { + // Dot expressions mean we're doing accessing properties of variables. + // If the variable is one of the definitely-safe global variables, then we're in the clear. + if (isSafeGlobal(prevId)) { + continue; + } + // If the node after this one is also a Dot expression, then this is a chained access, and we can't make + // any final judgements. + if (getNextSibling(child) instanceof ASTDotExpression) { + continue; + } + // If none of those things are true, then we need to determine whether the field being accessed is + // definitely safe. + ASTIdentifier propId = child.getFirstChildOfType(ASTIdentifier.class); + // If there's an identifier for a field/property, we need to check whether that property is inherently safe, + // either because it corresponds to a safe field or because its data type is known to be safe. + if (propId != null && !isSafeProperty(propId.getImage()) && !typedNodeIsSafe(propId)) { + // If the node isn't definitely safe, it ought to be escaped. Return false. + return false; + } + } else if (child instanceof ASTExpression) { + // Expressions should always be added to the list. + relevantChildren.add((ASTExpression) child); + } + } + // Just because there's nothing immediately wrong with this node doesn't mean its children are guaranteed to be + // fine. Iterate over all of the children and make a recursive call. If any of those calls return false, we need + // to relay that back up the chain. + for (ASTExpression e : relevantChildren) { + if (!expressionRecursivelyValid(e, escapes)) { + return false; + } + } + // If we didn't find a reason to return false, we're good. Return true. + return true; + } + + /** + * Indicates whether the provided function name corresponds to any of the provided escape functions. + * @param functionName - The name of a VF function + * @param escapes - A set of acceptable escape functions (e.g., JSENCODE, HTMLENCODE, etc) + * @return - True if the function is a viable escape. + */ + private static boolean functionIsEscape(String functionName, EnumSet escapes) { + // If one of the escapes we were passed is ANY, then we should replace the provided set with one that contains + // all possible escapes. + EnumSet handledEscapes = escapes.contains(Escaping.ANY) ? EnumSet.allOf(Escaping.class) : escapes; + for (Escaping e : handledEscapes) { + if (functionName.equalsIgnoreCase(e.toString())) { + return true; + } + } + return false; + } + + /** + * Certain built-in functions are inherently safe and don't require any escaping. This method determines whether the + * function name provided corresponds to one of those methods. + * @param functionName - The name of a VF function + * @return - True if the function is an inherently safe built-in function + */ + private static boolean functionInherentlySafe(String functionName) { + String lowerCaseName = functionName.toLowerCase(Locale.ROOT); + return SAFE_BUILTIN_FUNCTIONS.contains(lowerCaseName); + } + + /** + * Given a function name and a node containing its arguments, returns the ASTExpression nodes corresponding to arguments + * that require escaping. Frequently, this will be all arguments, but not always. + * @param functionName - The name of a function being called + * @param arguments - Contains the ASTExpression nodes representing the function's arguments + * @return - ASTExpression list containing all arguments that are vulnerable to XSS. + */ + private static List getXssableArguments(String functionName, ASTArguments arguments) { + List exprs = new ArrayList<>(); + int argCount = arguments.getNumChildren(); + if (argCount != 0) { + String lowerCaseName = functionName.toLowerCase(Locale.ROOT); + List indicesToCheck = new ArrayList<>(); + + // See if the function name corresponds to one of the built-in functions that don't require us to examine + // every argument. + if ("case".equals(lowerCaseName)) { + // CASE accepts (exx, val1, res1, val2, res2, ...else_res). We want resX and else_res. + for (int i = 2; i < argCount; i += 2) { + indicesToCheck.add(i); + } + indicesToCheck.add(argCount - 1); + } else if ("if".equals(lowerCaseName)) { + // IF accepts (test, if_true, if_false). We care about if_true and if_false. + indicesToCheck.add(1); + indicesToCheck.add(2); + } else { + boolean checkAllArgs = true; + // If this is a function with an XSSable first arg, add 0 to the array. + if (FUNCTIONS_WITH_XSSABLE_ARG0.contains(lowerCaseName)) { + checkAllArgs = false; + indicesToCheck.add(0); + } + // If this is a function that has at least 3 args, and the third arg is XSSable, add 2 to the array. + if (argCount > 2 && FUNCTIONS_WITH_XSSABLE_ARG2.contains(lowerCaseName)) { + checkAllArgs = false; + indicesToCheck.add(2); + } + // If the function has no known pattern for argument checking, all arguments must be checked. + if (checkAllArgs) { + for (int i = 0; i < argCount; i++) { + indicesToCheck.add(i); + } + } + } + + // Add each of the targeted arguments to the return array if they represent an Expression node. (They always + // should, but better safe than sorry.) + for (int i : indicesToCheck) { + VfNode ithArg = arguments.getChild(i); + if (ithArg instanceof ASTExpression) { + exprs.add((ASTExpression) ithArg); + } + } + } + return exprs; + } + + /** + * VF has global variables prefixed with a '$'. Some of those are inherently safe to access, and this method determines + * whether the provided ID corresponds to one of those globals. + * @param id - Identifier of some variable. + * @return - True if the global is inherently safe. + */ + private static boolean isSafeGlobal(String id) { + String lowerCaseId = id.toLowerCase(Locale.ROOT); + return SAFE_GLOBAL_VARS.contains(lowerCaseId); + } + + /** + * Determines whether the property being referenced is inherently safe, or if it requires XSS escaping. + * @param propertyName - The name of a field or property being referenced. + * @return - True if that field/property is inherently safe + */ + private static boolean isSafeProperty(String propertyName) { + String lowerCaseName = propertyName.toLowerCase(Locale.ROOT); + return SAFE_PROPERTIES.contains(lowerCaseName); + } + + private static boolean innerContainsSafeFields(final VfNode expression) { + for (VfNode child : expression.children()) { + + if (child instanceof ASTIdentifier && isSafeProperty(child.getImage())) { + return true; + } + + if (child instanceof ASTArguments) { + if (containsSafeFields((ASTArguments) child)) { + return true; + } + } + + if (child instanceof ASTDotExpression) { + if (innerContainsSafeFields((ASTDotExpression) child)) { + return true; + } + } + + } + + return false; + } + + public static boolean containsSafeFields(final VfNode expression) { + final ASTExpression ex = expression.getFirstChildOfType(ASTExpression.class); + + return ex != null && innerContainsSafeFields(ex); + } + + public static boolean startsWithSafeResource(final ASTElExpression el) { + final ASTExpression expression = el.getFirstChildOfType(ASTExpression.class); + if (expression != null) { + final ASTNegationExpression negation = expression.getFirstChildOfType(ASTNegationExpression.class); + if (negation != null) { + return true; + } + + final ASTIdentifier id = expression.getFirstChildOfType(ASTIdentifier.class); + if (id != null) { + List args = expression.findChildrenOfType(ASTArguments.class); + + if (!args.isEmpty()) { + return functionInherentlySafe(id.getImage()); + } else { + return isSafeGlobal(id.getImage()); + } + } + } + return false; + } + + public static boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, Escaping escape) { + return doesElContainAnyUnescapedIdentifiers(elExpression, EnumSet.of(escape)); + } + + public static boolean doesElContainAnyUnescapedIdentifiers(final ASTElExpression elExpression, + EnumSet escapes) { + if (elExpression == null) { + return false; + } + + final Set nonEscapedIds = new HashSet<>(); + + final List exprs = elExpression.findChildrenOfType(ASTExpression.class); + for (final ASTExpression expr : exprs) { + + if (innerContainsSafeFields(expr)) { + continue; + } + + if (expressionContainsSafeDataNodes(expr)) { + continue; + } + + final List ids = expr.findChildrenOfType(ASTIdentifier.class); + for (final ASTIdentifier id : ids) { + boolean isEscaped = false; + + for (Escaping e : escapes) { + + if (id.getImage().equalsIgnoreCase(e.toString())) { + isEscaped = true; + break; + } + + if (e.equals(Escaping.ANY)) { + for (Escaping esc : Escaping.values()) { + if (id.getImage().equalsIgnoreCase(esc.toString())) { + isEscaped = true; + break; + } + } + } + + } + + if (!isEscaped) { + nonEscapedIds.add(id); + } + } + + } + + return !nonEscapedIds.isEmpty(); + } + + /** + * Return true if the type of all data nodes can be determined and none of them require escaping + * @param expression + */ + private static boolean expressionContainsSafeDataNodes(ASTExpression expression) { + try { + for (VfTypedNode node : expression.getDataNodes().keySet()) { + if (!typedNodeIsSafe(node)) { + return false; + } + } + + return true; + } catch (ASTExpression.DataNodeStateException e) { + return false; + } + } + + private static boolean typedNodeIsSafe(VfTypedNode node) { + DataType dataType = node.getDataType(); + return dataType != null && !dataType.requiresEscaping; + } + + public enum Escaping { + HTMLENCODE("HTMLENCODE"), + URLENCODE("URLENCODE"), + JSINHTMLENCODE("JSINHTMLENCODE"), + JSENCODE("JSENCODE"), + ANY("ANY"); + + private final String text; + + Escaping(final String text) { + this.text = text; + } + + @Override + public String toString() { + return text; + } + } +} diff --git a/pmd-visualforce/src/main/resources/category/vf/security.xml b/pmd-visualforce/src/main/resources/category/vf/security.xml index fcb7c49a37..2f4408e929 100644 --- a/pmd-visualforce/src/main/resources/category/vf/security.xml +++ b/pmd-visualforce/src/main/resources/category/vf/security.xml @@ -26,6 +26,41 @@ Avoid calling VF action upon page load as the action becomes vulnerable to CSRF. + + +Checks for the correct encoding in `<style/>` tags in Visualforce pages. + +The rule is based on Salesforce Security's recommendation to prevent XSS in Visualforce as mentioned +on [Secure Coding Cross Site Scripting](https://developer.salesforce.com/docs/atlas.en-us.secure_coding_guide.meta/secure_coding_guide/secure_coding_cross_site_scripting.htm). + +In order to avoid cross site scripting, the relevant encoding must be used in HTML tags. The rule expects +`URLENCODING` or `JSINHTMLENCODING` for URL-based style values and any kind of encoding +(e.g. `HTMLENCODING`) for non-url style values. + +See also {% rule "VfUnescapeEl" %} to check escaping in other places on Visualforce pages. + + 3 + + + + +]]> + + + + + + + sObject field (from controller) assigned to url variable in style tag (html encoded) + 1 + + + + ]]> + + + + sObject field (from controller) assigned to url variable in style tag (JS encoded) + 1 + + + + ]]> + + + + sObject field (from controller) assigned to url variable in style tag (JSINHTML encoded) + 0 + + + + ]]> + + + + sObject field (from controller) assigned to url variable in style tag (URL encoded) + 0 + + + + ]]> + + + + sObject field (from controller) assigned to url variable in style tag + 1 + + + + ]]> + + + + sObject field (from controller) assigned to variable in style tag + 1 + + + + ]]> + + + + sObject field (from controller) assigned to variable in style tag with some encoding + 0 + + + + ]]> + + + + EL assigned to url variable in style tag within a safe resource + 0 + + + + ]]> + + + \ No newline at end of file diff --git a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/xml/VfUnescapeEl.xml b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/xml/VfUnescapeEl.xml index aec8e5e7d8..b09f596b3a 100644 --- a/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/xml/VfUnescapeEl.xml +++ b/pmd-visualforce/src/test/resources/net/sourceforge/pmd/lang/vf/rule/security/xml/VfUnescapeEl.xml @@ -437,22 +437,126 @@ - NOT method evaluates to safe boolean + IF() with literals in returns is safe 0 ]]> - JSON.parse method evaluates quoted EL to safe JSON + IF() with escaped XSS in returns is safe 0 + + + ]]> + + + + IF() with unescaped XSS in returns is unsafe + 2 + + + + ]]> + + + + CASE() with XSS in conditions is safe + 0 + + + + ]]> + + + + CASE() with escaped XSS in results is safe + 0 + + + + ]]> + + + + CASE() with unescaped XSS in results is unsafe + 1 + + + + ]]> + + + + LPAD() is not inherently safe + 3 + + + + ]]> + + + + Escaped LPAD() call is fine + 0 + + + + ]]> + + + + NOT, ISBLANK, BEGINS evaluate to safe booleans + 0 + + + + ]]> + + + + JSON.parse method evaluates quoted EL to unsafe XSS + 3 +