Skip to content

Redact password column from CSV import logging and error output - #1862

Open
jacalata wants to merge 1 commit into
developmentfrom
jac/csv-import-privacy
Open

Redact password column from CSV import logging and error output#1862
jacalata wants to merge 1 commit into
developmentfrom
jac/csv-import-privacy

Conversation

@jacalata

Copy link
Copy Markdown
Contributor

Summary

Fixes two paths in UserItem.CSVImport that could leak the password column from a user-import CSV file:

  1. _validate_import_line_or_throw logged logger.info(f"Reading user {line[:4]}"). The intent was to avoid printing the password, but slicing four characters only obscures usernames shorter than four characters -- longer usernames leak part of the row, and if the layout ever changed the password itself could reach the log.
  2. validate_file_for_import returned raw lines in its invalid_lines list when a row failed validation. Any caller that logged or surfaced those lines would expose the password unmasked.
  3. The per-column debug log inside _validate_import_line_or_throw printed the PASS column value verbatim.

Fixes #1829.

What changes

  • Row-level logs now include only the username (parsed by partition on the first comma) and are demoted to DEBUG.
  • invalid_lines entries pass through a new _redact_password_column helper that replaces column 1 with ***, preserving the line's original line ending.
  • The per-column DEBUG log masks the PASS column with ***.

The redaction helper uses the same naive str.split(",") parsing already used throughout the file. A password value containing commas is misaligned across columns; only the fragment landing in column 1 is masked. A test documents this limitation -- a proper CSV parser would be a larger change and is out of scope here.

Test plan

  • Four new tests in test/test_user_model.py covering: DEBUG log on a valid row (positive-and-negative assertion so a fix that only deletes the log line would fail), invalid-row path (both the returned invalid_lines and the logger output), embedded-comma limitation, and unit-level edges on _redact_password_column (LF, CRLF, no newline, empty password, trailing comma, single column).
  • Full test suite passes locally.

🤖 Generated with Claude Code

`UserItem.CSVImport.validate_file_for_import` and
`_validate_import_line_or_throw` wrote the raw CSV line -- including
the password column -- to any caller-supplied logger at INFO/DEBUG
level, and the whole raw line was pushed into the `invalid_lines`
list returned to callers when a row failed validation. Anyone using
the sample logger config or forwarding logs to a centralized system
would see clear-text passwords in the log stream.

Changes:
- `validate_file_for_import` logs only the username (column 0) at
  DEBUG, and calls a new `_redact_password_column` helper before
  appending an invalid row to the returned list.
- `_validate_import_line_or_throw` masks the PASS column value as
  `***` before logging it. Other column values still logged as-is
  for debugging.
- Both callers changed from INFO to DEBUG for these per-row messages;
  large imports were spamming operator-visible logs.
- Two regression tests capture logs and returned invalid_lines to
  assert the secret never appears in either place, plus a positive
  assertion that a `***` masked value IS logged so a future refactor
  that just removes the log line entirely doesn't pass.

Fixes #1829.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Coverage

Coverage Report
FileStmtsMissCoverMissing
tableauserverclient
   __init__.py50100% 
   config.py150100% 
   datetime_helpers.py2511 96%
   exponential_backoff.py200100% 
   filesys_helpers.py310100% 
   namespace.py2633 88%
tableauserverclient/bin
   __init__.py20100% 
   _version.py358212212 41%
tableauserverclient/helpers
   __init__.py10100% 
   logging.py20100% 
   strings.py3111 97%
tableauserverclient/models
   __init__.py460100% 
   collection_item.py4177 83%
   column_item.py553232 42%
   connection_credentials.py351111 69%
   connection_item.py941414 85%
   custom_view_item.py1442121 85%
   data_acceleration_report_item.py5411 98%
   data_alert_item.py15844 97%
   data_freshness_policy_item.py1551515 90%
   database_item.py2073636 83%
   datasource_item.py3001212 96%
   dqw_item.py10455 95%
   exceptions.py40100% 
   extensions_item.py13244 97%
   extract_item.py4444 91%
   favorites_item.py6988 88%
   fileupload_item.py190100% 
   flow_item.py1491010 93%
   flow_run_item.py710100% 
   group_item.py8966 93%
   groupset_item.py4977 86%
   interval_item.py1823232 82%
   job_item.py1921010 95%
   linked_tasks_item.py7911 99%
   location_item.py2922 93%
   metric_item.py1291313 90%
   oidc_item.py6333 95%
   pagination_item.py3411 97%
   permissions_item.py1111212 89%
   project_item.py2073131 85%
   property_decorators.py1001818 82%
   reference_item.py2622 92%
   revision_item.py5911 98%
   schedule_item.py20966 97%
   server_info_item.py3777 81%
   site_item.py6361313 98%
   subscription_item.py10122 98%
   table_item.py1191818 85%
   tableau_auth.py612525 59%
   tableau_types.py2711 96%
   tag_item.py150100% 
   target.py60100% 
   task_item.py5622 96%
   user_item.py3211717 95%
   view_item.py2201616 93%
   virtual_connection_item.py6488 88%
   webhook_item.py6911 99%
   workbook_item.py3621616 96%
tableauserverclient/server
   __init__.py90100% 
   exceptions.py40100% 
   filter.py2911 97%
   pager.py3311 97%
   query.py1431515 90%
   request_factory.py1335195195 85%
   request_options.py38655 99%
   server.py1882323 88%
   sort.py60100% 
tableauserverclient/server/endpoint
   __init__.py350100% 
   auth_endpoint.py771111 86%
   custom_views_endpoint.py1521212 92%
   data_acceleration_report_endpoint.py210100% 
   data_alert_endpoint.py942323 76%
   databases_endpoint.py1113030 73%
   datasources_endpoint.py3233333 90%
   default_permissions_endpoint.py4433 93%
   dqw_endpoint.py451616 64%
   endpoint.py2122020 91%
   exceptions.py7766 92%
   extensions_endpoint.py310100% 
   favorites_endpoint.py942222 77%
   fileuploads_endpoint.py510100% 
   flow_runs_endpoint.py6299 85%
   flow_task_endpoint.py2122 90%
   flows_endpoint.py1985353 73%
   groups_endpoint.py12699 93%
   groupsets_endpoint.py7277 90%
   jobs_endpoint.py6799 87%
   linked_tasks_endpoint.py370100% 
   metadata_endpoint.py881414 84%
   metrics_endpoint.py5566 89%
   oidc_endpoint.py4211 98%
   permissions_endpoint.py4433 93%
   projects_endpoint.py1782424 87%
   resource_tagger.py1273535 72%
   schedules_endpoint.py1191111 91%
   server_info_endpoint.py361010 72%
   sites_endpoint.py1302727 79%
   subscriptions_endpoint.py561414 75%
   tables_endpoint.py1103636 67%
   tasks_endpoint.py6366 90%
   users_endpoint.py18388 96%
   views_endpoint.py15099 94%
   virtual_connections_endpoint.py1131010 91%
   webhooks_endpoint.py5499 83%
   workbooks_endpoint.py3382222 93%
TOTAL12018142288% 

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens UserItem.CSVImport to prevent accidental disclosure of the CSV password column via logging and error/invalid-line output, addressing security issue #1829.

Changes:

  • Restrict row-level import logs to username-only and emit them at DEBUG level.
  • Redact the password column (PASS) when returning invalid_lines, and mask the PASS value in per-column DEBUG logs.
  • Add regression/unit tests covering log redaction behavior and helper edge cases; document the embedded-comma limitation; update changelog.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tableauserverclient/models/user_item.py Adds password redaction helper and updates CSV import logging/invalid-line handling to avoid password disclosure.
test/test_user_model.py Adds regression tests ensuring passwords do not appear in DEBUG logs or invalid row output; adds unit tests for redaction helper behavior.
CHANGELOG.md Documents the security fix for CSV import password logging/output behavior (Fixes #1829).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +497 to +502
trailing_newline = "\n" if line.endswith("\n") else ""
fields = line.rstrip("\n").split(",")
pass_index = UserItem.CSVImport.ColumnType.PASS.value
if len(fields) > pass_index:
fields[pass_index] = "***"
return ",".join(fields) + trailing_newline
Comment thread test/test_user_model.py
Comment on lines +197 to +200
# CRLF-terminated (the \r rides with the last field, ending is preserved)
assert redact("jsmith,hunter2,fname\r\n") == "jsmith,***,fname\r\n"
# No trailing newline
assert redact("jsmith,hunter2,fname") == "jsmith,***,fname"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: _validate_import_line_or_throw logs credential fields at DEBUG level

2 participants