pwpolicy_special_character_enforce: enforce more than 1 special character. #70

Closed
opened 2026-01-19 18:29:08 +00:00 by michael · 6 comments
Owner

Originally created by @DavidRvrsR3 on GitHub.

Originally assigned to: @robertgendler on GitHub.

Summary

Rule: pwpolicy_special_character_enforce is giving false positive if you enforce more than 1 special character.
So if you push a password policy requiring 2 or more special characters a failure will be audited.

So to reproduce sent a Configuration Profile with com.apple.mobiledevice.passwordpolicy minComplexChars 2,3 or 4 will fail

Operating System version

15.2 Beta (24C5079e), it is a small oversight in the script, so will impact all macOS Versions and all architectures.

Intel or Apple Silicon

tested on Silicon but will be the case for all.

What is the current bug behavior?

We are enforcing more than 1 special character so the line that is being checked is slightly different as the check script is looking for but still should qualify as compliant according to the documentation. It is looking for:
'\''(.*[^a-zA-Z0-9].*){1,}'\''")])'
but it will find
'\''(.*[^a-zA-Z0-9].*){3,}'\''")])'.
It than flags pwpolicy_special_character_enforce as failed.

What is the expected correct behavior?

pwpolicy_special_character_enforce should pass if a value above 0 is given

Possible fixes

#####----- Rule: pwpolicy_special_character_enforce -----#####
## Addresses the following NIST 800-53 controls: 
# * IA-5(1)
rule_arch=""
if [[ "$arch" == "$rule_arch" ]] || [[ -z "$rule_arch" ]]; then
  special_chars=$(/usr/bin/pwpolicy -getaccountpolicies 2> /dev/null | \
      /usr/bin/tail +2 | \
      /usr/bin/xmllint --xpath '//*[contains(text(),"policyAttributePassword matches")]/text()' - | \
      grep -oE "\(.*\[\^a-zA-Z0-9\].*\)\{[0-9]+," | \
      grep -oE '\{[0-9]+,' | \
      tr -d '{,')
    
  if [[ $special_chars -ge 1 ]]; then
    result_value="true"
  else
    result_value="false"
  fi

  # expected result {'string': 'true'}

  # check to see if rule is exempt
  unset exempt
  unset exempt_reason

  exempt=$(/usr/bin/osascript -l JavaScript << EOS 2>/dev/null
ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('org.cis_lvl2.audit').objectForKey('pwpolicy_special_character_enforce'))["exempt"]
EOS
)
    exempt_reason=$(/usr/bin/osascript -l JavaScript << EOS 2>/dev/null
ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('org.cis_lvl2.audit').objectForKey('pwpolicy_special_character_enforce'))["exempt_reason"]
EOS
)   
    customref="$(echo "pwpolicy_special_character_enforce" | rev | cut -d ' ' -f 2- | rev)"
    customref="$(echo "$customref" | tr " " ",")"
    if [[ $result_value == "true" ]]; then
        logmessage "pwpolicy_special_character_enforce passed (Result: $result_value, Expected: \"{'string': 'true'}\")"
        /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool NO
        if [[ ! "$customref" == "pwpolicy_special_character_enforce" ]]; then
            /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add reference -string "$customref"
        fi
        /usr/bin/logger "mSCP: cis_lvl2 - pwpolicy_special_character_enforce passed (Result: $result_value, Expected: "{'string': 'true'}")"
    else
        if [[ ! $exempt == "1" ]] || [[ -z $exempt ]];then
            logmessage "pwpolicy_special_character_enforce failed (Result: $result_value, Expected: \"{'string': 'true'}\")"
            /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool YES
            if [[ ! "$customref" == "pwpolicy_special_character_enforce" ]]; then
                /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add reference -string "$customref"
            fi
            /usr/bin/logger "mSCP: cis_lvl2 - pwpolicy_special_character_enforce failed (Result: $result_value, Expected: "{'string': 'true'}")"
        else
            logmessage "pwpolicy_special_character_enforce failed (Result: $result_value, Expected: \"{'string': 'true'}\") - Exemption Allowed (Reason: \"$exempt_reason\")"
            /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool YES
            if [[ ! "$customref" == "pwpolicy_special_character_enforce" ]]; then
              /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add reference -string "$customref"
            fi
            /usr/bin/logger "mSCP: cis_lvl2 - pwpolicy_special_character_enforce failed (Result: $result_value, Expected: "{'string': 'true'}") - Exemption Allowed (Reason: "$exempt_reason")"
            /bin/sleep 1
        fi
    fi


else
    logmessage "pwpolicy_special_character_enforce does not apply to this architecture"
    /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool NO
fi
Originally created by @DavidRvrsR3 on GitHub. Originally assigned to: @robertgendler on GitHub. ### Summary Rule: pwpolicy_special_character_enforce is giving false positive if you enforce more than 1 special character. So if you push a password policy requiring 2 or more special characters a failure will be audited. So to reproduce sent a Configuration Profile with com.apple.mobiledevice.passwordpolicy minComplexChars 2,3 or 4 will fail ### Operating System version 15.2 Beta (24C5079e), it is a small oversight in the script, so will impact all macOS Versions and all architectures. ### Intel or Apple Silicon tested on Silicon but will be the case for all. ### What is the current *bug* behavior? We are enforcing more than 1 special character so the line that is being checked is slightly different as the check script is looking for but still should qualify as compliant according to the documentation. It is looking for: ``` '\''(.*[^a-zA-Z0-9].*){1,}'\''")])'``` but it will find ``` '\''(.*[^a-zA-Z0-9].*){3,}'\''")])'. ``` It than flags pwpolicy_special_character_enforce as failed. ### What is the expected *correct* behavior? pwpolicy_special_character_enforce should pass if a value above 0 is given ### Possible fixes ``` #####----- Rule: pwpolicy_special_character_enforce -----##### ## Addresses the following NIST 800-53 controls: # * IA-5(1) rule_arch="" if [[ "$arch" == "$rule_arch" ]] || [[ -z "$rule_arch" ]]; then special_chars=$(/usr/bin/pwpolicy -getaccountpolicies 2> /dev/null | \ /usr/bin/tail +2 | \ /usr/bin/xmllint --xpath '//*[contains(text(),"policyAttributePassword matches")]/text()' - | \ grep -oE "\(.*\[\^a-zA-Z0-9\].*\)\{[0-9]+," | \ grep -oE '\{[0-9]+,' | \ tr -d '{,') if [[ $special_chars -ge 1 ]]; then result_value="true" else result_value="false" fi # expected result {'string': 'true'} # check to see if rule is exempt unset exempt unset exempt_reason exempt=$(/usr/bin/osascript -l JavaScript << EOS 2>/dev/null ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('org.cis_lvl2.audit').objectForKey('pwpolicy_special_character_enforce'))["exempt"] EOS ) exempt_reason=$(/usr/bin/osascript -l JavaScript << EOS 2>/dev/null ObjC.unwrap($.NSUserDefaults.alloc.initWithSuiteName('org.cis_lvl2.audit').objectForKey('pwpolicy_special_character_enforce'))["exempt_reason"] EOS ) customref="$(echo "pwpolicy_special_character_enforce" | rev | cut -d ' ' -f 2- | rev)" customref="$(echo "$customref" | tr " " ",")" if [[ $result_value == "true" ]]; then logmessage "pwpolicy_special_character_enforce passed (Result: $result_value, Expected: \"{'string': 'true'}\")" /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool NO if [[ ! "$customref" == "pwpolicy_special_character_enforce" ]]; then /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add reference -string "$customref" fi /usr/bin/logger "mSCP: cis_lvl2 - pwpolicy_special_character_enforce passed (Result: $result_value, Expected: "{'string': 'true'}")" else if [[ ! $exempt == "1" ]] || [[ -z $exempt ]];then logmessage "pwpolicy_special_character_enforce failed (Result: $result_value, Expected: \"{'string': 'true'}\")" /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool YES if [[ ! "$customref" == "pwpolicy_special_character_enforce" ]]; then /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add reference -string "$customref" fi /usr/bin/logger "mSCP: cis_lvl2 - pwpolicy_special_character_enforce failed (Result: $result_value, Expected: "{'string': 'true'}")" else logmessage "pwpolicy_special_character_enforce failed (Result: $result_value, Expected: \"{'string': 'true'}\") - Exemption Allowed (Reason: \"$exempt_reason\")" /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool YES if [[ ! "$customref" == "pwpolicy_special_character_enforce" ]]; then /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add reference -string "$customref" fi /usr/bin/logger "mSCP: cis_lvl2 - pwpolicy_special_character_enforce failed (Result: $result_value, Expected: "{'string': 'true'}") - Exemption Allowed (Reason: "$exempt_reason")" /bin/sleep 1 fi fi else logmessage "pwpolicy_special_character_enforce does not apply to this architecture" /usr/bin/defaults write "$audit_plist" pwpolicy_special_character_enforce -dict-add finding -bool NO fi ```
Author
Owner

@brodjieski commented on GitHub:

Sure thing!
It looks like the check is actually out of sync with what CIS has in their benchmark. We can look to update the check itself to match what CIS has, which looks to be similar to what you are suggesting.

We do have plans to update this, and all of the password policy rules to better reflect guidance from NIST. Hopefully we can include that in the next release. I'll keep this issue opened to track these updates. Look for a commit with changes that can be tested in the coming days.

Thanks again!

@brodjieski commented on GitHub: Sure thing! It looks like the check is actually out of sync with what CIS has in their benchmark. We can look to update the check itself to match what CIS has, which looks to be similar to what you are suggesting. We do have plans to update this, and all of the password policy rules to better reflect guidance from NIST. Hopefully we can include that in the next release. I'll keep this issue opened to track these updates. Look for a commit with changes that can be tested in the coming days. Thanks again!
Author
Owner

@brodjieski commented on GitHub:

Hello!
This rule has an organization defined value (ODV), which has a recommended value of 1 (same for CIS level 2). If you tailor this ODV value to 2, or 3, or whatever you prefer, then regenerate the documents and scripts, it should be updated to reflect that value.

However, if you run the compliance scripts without tailoring the ODV, but then deploy a configuration that contains a different value than expected, the script will produce a finding.

@brodjieski commented on GitHub: Hello! This rule has an organization defined value (ODV), which has a recommended value of 1 (same for CIS level 2). If you tailor this ODV value to 2, or 3, or whatever you prefer, then regenerate the documents and scripts, it should be updated to reflect that value. However, if you run the compliance scripts without tailoring the ODV, but then deploy a configuration that contains a different value than expected, the script will produce a finding.
Author
Owner

@DavidRvrsR3 commented on GitHub:

Thanks for explaining the ODV system and how it's meant to be tailored at the organization level. I get that it’s flexible by design, but I think there’s a good opportunity here to make things easier for everyone.
Right now, the script flags stricter policies like {2,} or {3,} as non-compliant, even though they meet or exceed the standards. If we tweak the script to recognize any value greater than 0, it would save a lot of time and hassle—both during audits and in day-to-day compliance checks.
This way, admins wouldn’t need to manually adjust the script with every update or deal with false positives, and the system would still be just as flexible for those who want to customize it. I’m happy to help with testing or tweaking the logic if needed. Let me know what you think!

@DavidRvrsR3 commented on GitHub: Thanks for explaining the ODV system and how it's meant to be tailored at the organization level. I get that it’s flexible by design, but I think there’s a good opportunity here to make things easier for everyone. Right now, the script flags stricter policies like {2,} or {3,} as non-compliant, even though they meet or exceed the standards. If we tweak the script to recognize any value greater than 0, it would save a lot of time and hassle—both during audits and in day-to-day compliance checks. This way, admins wouldn’t need to manually adjust the script with every update or deal with false positives, and the system would still be just as flexible for those who want to customize it. I’m happy to help with testing or tweaking the logic if needed. Let me know what you think!
Author
Owner

@DavidRvrsR3 commented on GitHub:

result_value=$( [[ $(/usr/bin/pwpolicy -getaccountpolicies 2>/dev/null | /usr/bin/tail +2 | /usr/bin/xmllint --xpath '//*[contains(text(),"policyAttributePassword matches")]/text()' - 2>/dev/null | grep -oE '\{[0-9]+,' | tr -d '{,' | tr -d '\n') -gt 0 ]] && echo "true" || echo "false" )

if you require a single line solution

@DavidRvrsR3 commented on GitHub: ```result_value=$( [[ $(/usr/bin/pwpolicy -getaccountpolicies 2>/dev/null | /usr/bin/tail +2 | /usr/bin/xmllint --xpath '//*[contains(text(),"policyAttributePassword matches")]/text()' - 2>/dev/null | grep -oE '\{[0-9]+,' | tr -d '{,' | tr -d '\n') -gt 0 ]] && echo "true" || echo "false" ) ``` if you require a single line solution
Author
Owner

@robertgendler commented on GitHub:

/usr/bin/pwpolicy -getaccountpolicies 2>/dev/null | /usr/bin/tail -n +2 | /usr/bin/xmllint --xpath "//string[contains(text(), \"policyAttributePassword matches '(.*[^a-zA-Z0-9].*){\")]" - 2>/dev/null | /usr/bin/awk -F"{|}" '{if ($2 >= $ODV) {print "true"} else {print "false"}}'

1 Line. You'll need to directly replace ODV with the value you want to test due to the replacement in Awk. You can't just do ODV=1 in a shell.

@robertgendler commented on GitHub: `/usr/bin/pwpolicy -getaccountpolicies 2>/dev/null | /usr/bin/tail -n +2 | /usr/bin/xmllint --xpath "//string[contains(text(), \"policyAttributePassword matches '(.*[^a-zA-Z0-9].*){\")]" - 2>/dev/null | /usr/bin/awk -F"{|}" '{if ($2 >= $ODV) {print "true"} else {print "false"}}'` 1 Line. You'll need to directly replace ODV with the value you want to test due to the replacement in Awk. You can't just do ODV=1 in a shell.
Author
Owner

@robertgendler commented on GitHub:

Closing cause this is completed.

@robertgendler commented on GitHub: Closing cause this is completed.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: usnistgov/macos_security#70