Skip to content

Added pam misconfig detector#1705

Open
mzfr wants to merge 5 commits intogoogle:mainfrom
mzfr:pam_misconfig
Open

Added pam misconfig detector#1705
mzfr wants to merge 5 commits intogoogle:mainfrom
mzfr:pam_misconfig

Conversation

@mzfr
Copy link
Contributor

@mzfr mzfr commented Jan 22, 2026

Fixes #1264

Also please let me know if I should split either the tests or the main detector logic in multiple files

}

// PAM control flags that can cause authentication bypass when misconfigured.
// Reference: http://www.linux-pam.org/Linux-PAM-html/sag-configuration-file.html
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m getting a 404 when I click the link.

type Detector struct{}

// New returns a new PAM misconfiguration detector.
func New() detector.Detector {
Copy link
Collaborator

@alessandro-Doyensec alessandro-Doyensec Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constructor is never called throughout the project. Every plugin should be registered in a list.go file, see: https://github.com/google/osv-scalibr/blob/main/detector/list/list.go

authEntries = append(authEntries, *entry)
}

if lineIssues := analyzePAMLine(filePath, lineNum, line, isLegacyFormat); len(lineIssues) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why the entry isn't directly passed to the analyzePAMLine function?

}

// analyzePAMLine analyzes a single PAM configuration line for security issues.
func analyzePAMLine(filePath string, lineNum int, line string, isLegacyFormat bool) []string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's possible to directly pass the PAM entry to this function as an argument (ref #1705 (comment)), rename this analyzePAMEntry

control string // required, sufficient, optional, etc.
modulePath string // e.g., pam_unix.so, pam_permit.so
args []string // module arguments
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move parsePAMLine right below the pamEntry definition.

return false
}

type controlEffect struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, document what a controlEffect is (providing an example)

isOptionalOnly bool
}

func parseControlEffect(control string) controlEffect {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can the parseControlEffect logic be moved inside the parsePAMLine function?

I would expect the pamEntry struct to be something like:

type pamEntry struct {
	moduleType string        // auth, account, password, session
	control    controlEffect // required, sufficient, optional, etc.
	modulePath string        // e.g., pam_unix.so, pam_permit.so
	args       []string      // module arguments
}

This way the parsing and checking step could be completely separated.

@alessandro-Doyensec
Copy link
Collaborator

Please, Add the detector to the docs/supported_inventory_types.md.

@alessandro-Doyensec
Copy link
Collaborator

Hi @mzfr

Thanks for the pull request!

I’ve reviewed the code and left a few comments for you to address.

Add misc/pammisconfig to the Linux detector list and supported plugins doc. Refactor PAM parsing to compute control effects once per entry.
var Misc = concat(
InitMap{
cronjobprivesc.Name: {noCFG(cronjobprivesc.New)},
dockersocket.Name: {noCFG(dockersocket.New)},
Copy link
Collaborator

@alessandro-Doyensec alessandro-Doyensec Feb 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Directly add the plugins registration here (remove list/misc_linux.go and list/misc_other.go):

pammisconfig.Name: {noCFG(pammisconfig.New)},

As a result TestDetectorsFromName/Find_misc_detectors will fail, to fix it just add: ""misc/pammisconfig" into the wantDets.

return "password authentication"
}

func checkOptionalOnlyAuth(filePath string, entries []pamEntry) []string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, document the checkOptionalOnlyAuth function

type pamEntry struct {
moduleType string // auth, account, password, session
control string // required, sufficient, optional, etc.
controlEff controlEffect
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the controlEff field always populated? If not make it a pointer to controlEffect (using *controlEffect as type).

@alessandro-Doyensec
Copy link
Collaborator

Hi @mzfr

Thanks for the changes.

I left a couple more comments for you to address

Register misc/pammisconfig in the main misc list, update list tests, and document/nil-guard control effects.
// and when combined with 'sufficient', those users bypass further auth checks.
// Reference: https://unix.stackexchange.com/a/767197
if isModule(entry.modulePath, "pam_succeed_if.so") {
control := parsedControlEffect(entry.controlEff)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a nil-check for .controlEff and skip the if corpus if it's missing?

I’m also fine with rolling the type back to controlEff (removing the pointer) as long as the default values are coherent

@alessandro-Doyensec
Copy link
Collaborator

Hi @mzfr

Thanks for the change, I left a comment for you to check.

I see in the code that you are using the .controlEffect as if the pointer was not there.

Ideally you could check if entry.controlEff is nil and return early since neither Check 1 and Check 2 make sense in that case.

isSufficientLike bool
skipNext int
isOptionalOnly bool
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add the following helper methods to the controllEffect type:

func (c *controlEffect) ShortCircuits() bool {
	if c == nil {
		return false
	}
	return c.isSufficientLike || c.skipNext > 0
}

func (c *controlEffect) IsOptionalOnly() bool {
	if c == nil {
		return false
	}
	return c.isOptionalOnly
}

Feel free to rename the ShortCircuits as you deem corrrect

// pam_permit.so ALWAYS returns success, so using it with bypass controls
// means any user can authenticate without a valid password.
// Reference: https://serverfault.com/questions/890012/pam-accepting-any-password-for-valid-users
if hasControlEffect && isModule(entry.modulePath, "pam_permit.so") {
Copy link
Collaborator

@alessandro-Doyensec alessandro-Doyensec Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use

if entry.controlEff.ShortCircuits() && isModule(entry.modulePath, "pam_permit.so")

here

// pam_succeed_if.so can match broad user groups (e.g., uid >= 1000),
// and when combined with 'sufficient', those users bypass further auth checks.
// Reference: https://unix.stackexchange.com/a/767197
if hasControlEffect && isModule(entry.modulePath, "pam_succeed_if.so") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use

if entry.controlEff.ShortCircuits() && isModule(entry.modulePath, "pam_succeed_if.so")

here

return false
}

func parsedControlEffect(effect *controlEffect) controlEffect {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, remove this function


var hasEffectiveNonOptional bool
var hasPermitOptional bool
for _, entry := range entries {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, adjust to use:

entry.controlEff.IsOptionalOnly()

@alessandro-Doyensec
Copy link
Collaborator

Hello @mzfr

Thanks for the changes, I left a couple of comments regarding the addition of helper function which should simplify the logic and remove some nesting from the code.

Thanks again!

Use nil-safe controlEffect helpers and remove duplicate parsing logic
Copy link
Collaborator

@alessandro-Doyensec alessandro-Doyensec left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @mzfr

Thanks for the changes! Looks good to merge now.

@alessandro-Doyensec alessandro-Doyensec added the lgtm The PR has been reviewed and approved ("Looks Good To Me") by vendors helping with code reviews. label Feb 17, 2026
mzfr added a commit to mzfr/security-testbeds that referenced this pull request Feb 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm The PR has been reviewed and approved ("Looks Good To Me") by vendors helping with code reviews.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PRP: Detector for PAM Misconfiguration

2 participants